From a5d426989c3b31f657d571a4a34b02e5e27e247c Mon Sep 17 00:00:00 2001 From: peptiderepo Date: Sun, 12 Apr 2026 08:35:01 +0700 Subject: [PATCH] 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 --- tests/bootstrap.php | 11 +++++++++ tests/unit/BaseTestCase.php | 15 ++++++++++++ tests/unit/Core/CostTrackerTest.php | 18 +++++++------- .../unit/Providers/OpenRouterPricingTest.php | 24 +++++++++++++++++++ .../unit/Providers/OpenRouterProviderTest.php | 23 ++++++++++-------- 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 8f2bbe6..c10a5bc 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,6 +29,17 @@ if ( ! defined( 'HOUR_IN_SECONDS' ) ) { 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. if ( ! defined( 'ARRAY_A' ) ) { define( 'ARRAY_A', 'ARRAY_A' ); diff --git a/tests/unit/BaseTestCase.php b/tests/unit/BaseTestCase.php index f91d7ff..4a9cf68 100644 --- a/tests/unit/BaseTestCase.php +++ b/tests/unit/BaseTestCase.php @@ -32,12 +32,27 @@ abstract class BaseTestCase extends TestCase { Functions\when( 'get_transient' )->justReturn( false ); Functions\when( 'set_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. */ 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(); parent::tearDown(); } diff --git a/tests/unit/Core/CostTrackerTest.php b/tests/unit/Core/CostTrackerTest.php index 16f2d2f..d67300a 100644 --- a/tests/unit/Core/CostTrackerTest.php +++ b/tests/unit/Core/CostTrackerTest.php @@ -142,23 +142,23 @@ class CostTrackerTest extends BaseTestCase { $tracker = new \PRAutoBlogger_Cost_Tracker(); $tracker->set_run_id( 'run_test' ); - // Mock wpdb to expect an insert call. - $this->wpdb->expects( $this->once() ) + // Allow multiple inserts — log_api_call inserts a generation log row, + // and Logger may also insert an event log row if a warning fires. + $this->wpdb->expects( $this->atLeastOnce() ) ->method( 'insert' ) ->willReturn( 1 ); - // Signature: log_api_call(?int $post_id, string $stage, string $provider, - // string $model, int $prompt_tokens, int $completion_tokens, - // float $estimated_cost, float $duration_seconds). + // Actual signature: log_api_call(?int $post_id, string $stage, + // string $provider, string $model, int $prompt_tokens, + // 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( 123, 'analysis', 'openrouter', - 'openai/gpt-4', + 'openai/gpt-4o', 500, - 250, - 0.01, - 1.5 + 250 ); // If we get here without exception, the method worked. diff --git a/tests/unit/Providers/OpenRouterPricingTest.php b/tests/unit/Providers/OpenRouterPricingTest.php index f8c049f..81a2973 100644 --- a/tests/unit/Providers/OpenRouterPricingTest.php +++ b/tests/unit/Providers/OpenRouterPricingTest.php @@ -14,9 +14,28 @@ use Brain\Monkey\Functions; class OpenRouterPricingTest extends BaseTestCase { + /** + * @var \PHPUnit\Framework\MockObject\MockObject Mock $wpdb for Logger. + */ + private $wpdb; + protected function setUp(): void { 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. // Return a valid cached model list so it doesn't try to make HTTP calls. Functions\when( 'get_transient' )->alias( @@ -44,6 +63,11 @@ class OpenRouterPricingTest extends BaseTestCase { Functions\when( 'set_transient' )->justReturn( true ); } + protected function tearDown(): void { + unset( $GLOBALS['wpdb'] ); + parent::tearDown(); + } + /** * Test Pricing can be instantiated. */ diff --git a/tests/unit/Providers/OpenRouterProviderTest.php b/tests/unit/Providers/OpenRouterProviderTest.php index fc49f3b..b650bd2 100644 --- a/tests/unit/Providers/OpenRouterProviderTest.php +++ b/tests/unit/Providers/OpenRouterProviderTest.php @@ -21,8 +21,10 @@ class OpenRouterProviderTest extends BaseTestCase { // Stub get_option — provider calls get_option('prautoblogger_openrouter_api_key', '') // for API key retrieval via private get_api_key() method. // Return empty string so decryption is skipped (no key = empty return). + // Also include prautoblogger_log_level for Logger singleton. $this->stub_get_option( [ 'prautoblogger_openrouter_api_key' => '', + 'prautoblogger_log_level' => 'info', ] ); // 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(); $messages = [ [ 'role' => 'user', 'content' => 'Test message' ], ]; - $result = $provider->send_chat_completion( $messages, 'model/test', [] ); - - $this->assertIsArray( $result ); + $this->expectException( \RuntimeException::class ); + $provider->send_chat_completion( $messages, 'model/test', [] ); } /** - * 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(); $messages = [ @@ -156,8 +160,7 @@ class OpenRouterProviderTest extends BaseTestCase { ]; $options = [ 'temperature' => 0.7 ]; - $result = $provider->send_chat_completion( $messages, 'model/test', $options ); - - $this->assertIsArray( $result ); + $this->expectException( \RuntimeException::class ); + $provider->send_chat_completion( $messages, 'model/test', $options ); } }