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:
@@ -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' );
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user