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