v0.9.0: Runware FLUX.1 schnell as default image provider
New Runware image backend at ~$0.0006/image (FLUX.1 schnell) — 65× cheaper than Gemini 2.5 Flash Image. Provider mirrors OpenRouter's split: interface + provider + support + pricing + batch, all under the 300-line cap. Batch class uses curl_multi for true parallel A/B dispatch — wall-clock drops to the slowest single image instead of the sum. Changes: - Add PRAutoBlogger_Runware_Image_Provider + Pricing + Support + Batch - Flip PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER/MODEL to runware:100@1 - Add prautoblogger_runware_api_key settings field (encrypted at rest) - v0.9.0 one-time migration flips legacy default model + re-derives provider - Wire runware branch into Image_Pipeline::create_default_provider() - Add runware:100@1 and runware:101@1 to Image_Model_Registry (defaults top) - Update image prompt builder: no more anthropomorphized molecules; prefer concrete human-scale scenes (gym bros, doctors, scientists) - ARCHITECTURE.md + CHANGELOG.md updated with new integration + decision - Unit tests for pricing, support, provider; update model registry test Cost impact at 100 images/day (50 articles × A/B): - Before (Gemini via OpenRouter): $3.90/day ≈ $117/mo - After (FLUX schnell via Runware): $0.06/day ≈ $1.80/mo Net saving: ~$115/mo
This commit is contained in:
184
tests/unit/Providers/RunwareImageSupportTest.php
Normal file
184
tests/unit/Providers/RunwareImageSupportTest.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRAutoBlogger_Runware_Image_Support.
|
||||
*
|
||||
* Covers response parsing (URL extraction), dimension snapping to FLUX
|
||||
* constraints, seed normalization, and API-key lookup.
|
||||
*
|
||||
* @package PRAutoBlogger\Tests\Providers
|
||||
*/
|
||||
|
||||
namespace PRAutoBlogger\Tests\Providers;
|
||||
|
||||
use PRAutoBlogger\Tests\BaseTestCase;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class RunwareImageSupportTest extends BaseTestCase {
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
Functions\when( 'esc_html' )->returnArg();
|
||||
Functions\when( 'esc_html__' )->returnArg();
|
||||
Functions\when( '__' )->returnArg();
|
||||
|
||||
if ( ! class_exists( 'PRAutoBlogger_Encryption' ) ) {
|
||||
eval( '
|
||||
class PRAutoBlogger_Encryption {
|
||||
public static function is_encrypted( $v ) { return 0 === strpos( $v, "enc:" ); }
|
||||
public static function decrypt( $v ) { return "rw-test-key-123"; }
|
||||
public static function encrypt( $v ) { return "enc:" . $v; }
|
||||
}
|
||||
' );
|
||||
}
|
||||
}
|
||||
|
||||
/** A well-formed imageInference entry returns its imageURL. */
|
||||
public function test_extract_image_url_valid_response(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$raw = wp_json_encode( [
|
||||
'data' => [
|
||||
[
|
||||
'taskType' => 'imageInference',
|
||||
'imageURL' => 'https://im.runware.ai/image/abc123.png',
|
||||
],
|
||||
],
|
||||
] );
|
||||
$this->assertSame(
|
||||
'https://im.runware.ai/image/abc123.png',
|
||||
$support->extract_image_url( $raw )
|
||||
);
|
||||
}
|
||||
|
||||
/** An errors[] array in the response causes a RuntimeException. */
|
||||
public function test_extract_image_url_error_response_throws(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$raw = wp_json_encode( [
|
||||
'errors' => [
|
||||
[ 'message' => 'invalid api key' ],
|
||||
],
|
||||
] );
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$this->expectExceptionMessage( 'invalid api key' );
|
||||
$support->extract_image_url( $raw );
|
||||
}
|
||||
|
||||
/** Malformed JSON throws RuntimeException. */
|
||||
public function test_extract_image_url_invalid_json_throws(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$support->extract_image_url( 'not json at all' );
|
||||
}
|
||||
|
||||
/** Data array present but no imageInference entry throws. */
|
||||
public function test_extract_image_url_no_inference_entry_throws(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$raw = wp_json_encode( [
|
||||
'data' => [
|
||||
[ 'taskType' => 'authentication' ],
|
||||
],
|
||||
] );
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$support->extract_image_url( $raw );
|
||||
}
|
||||
|
||||
/** imageInference present but empty imageURL throws. */
|
||||
public function test_extract_image_url_empty_url_throws(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$raw = wp_json_encode( [
|
||||
'data' => [
|
||||
[ 'taskType' => 'imageInference', 'imageURL' => '' ],
|
||||
],
|
||||
] );
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$support->extract_image_url( $raw );
|
||||
}
|
||||
|
||||
/** snap_dimension rounds to nearest multiple of 64. */
|
||||
public function test_snap_dimension_rounds_to_64(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 1216, $support->snap_dimension( 1200 ) );
|
||||
$this->assertSame( 640, $support->snap_dimension( 632 ) );
|
||||
$this->assertSame( 1024, $support->snap_dimension( 1024 ) );
|
||||
$this->assertSame( 1024, $support->snap_dimension( 1050 ) );
|
||||
}
|
||||
|
||||
/** snap_dimension clamps to minimum 512. */
|
||||
public function test_snap_dimension_clamps_min(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 512, $support->snap_dimension( 64 ) );
|
||||
$this->assertSame( 512, $support->snap_dimension( 200 ) );
|
||||
}
|
||||
|
||||
/** snap_dimension clamps to maximum 2048. */
|
||||
public function test_snap_dimension_clamps_max(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 2048, $support->snap_dimension( 4096 ) );
|
||||
$this->assertSame( 2048, $support->snap_dimension( 2500 ) );
|
||||
}
|
||||
|
||||
/** A valid positive seed passes through unchanged. */
|
||||
public function test_normalize_seed_positive_passthrough(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 42, $support->normalize_seed( 42 ) );
|
||||
$this->assertSame( 1234567, $support->normalize_seed( 1234567 ) );
|
||||
}
|
||||
|
||||
/** Null seed derives a positive integer. */
|
||||
public function test_normalize_seed_null_derives_positive(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$seed = $support->normalize_seed( null );
|
||||
$this->assertGreaterThan( 0, $seed );
|
||||
}
|
||||
|
||||
/** Zero seed is replaced with a derived positive integer. */
|
||||
public function test_normalize_seed_zero_replaced(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$seed = $support->normalize_seed( 0 );
|
||||
$this->assertGreaterThan( 0, $seed );
|
||||
}
|
||||
|
||||
/** Negative seed is replaced with a derived positive integer. */
|
||||
public function test_normalize_seed_negative_replaced(): void {
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$seed = $support->normalize_seed( -5 );
|
||||
$this->assertGreaterThan( 0, $seed );
|
||||
}
|
||||
|
||||
/** Missing API key option throws RuntimeException. */
|
||||
public function test_get_api_key_missing_throws(): void {
|
||||
Functions\when( 'get_option' )->alias( function ( $key, $default = false ) {
|
||||
return $default;
|
||||
} );
|
||||
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->expectException( \RuntimeException::class );
|
||||
$support->get_api_key();
|
||||
}
|
||||
|
||||
/** Encrypted key is decrypted on read. */
|
||||
public function test_get_api_key_decrypts_encrypted(): void {
|
||||
Functions\when( 'get_option' )->alias( function ( $key, $default = false ) {
|
||||
if ( 'prautoblogger_runware_api_key' === $key ) {
|
||||
return 'enc:stored-ciphertext';
|
||||
}
|
||||
return $default;
|
||||
} );
|
||||
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 'rw-test-key-123', $support->get_api_key() );
|
||||
}
|
||||
|
||||
/** A plaintext key (pre-migration) passes through unchanged. */
|
||||
public function test_get_api_key_plaintext_passthrough(): void {
|
||||
Functions\when( 'get_option' )->alias( function ( $key, $default = false ) {
|
||||
if ( 'prautoblogger_runware_api_key' === $key ) {
|
||||
return 'rw-plain-key';
|
||||
}
|
||||
return $default;
|
||||
} );
|
||||
|
||||
$support = new \PRAutoBlogger_Runware_Image_Support();
|
||||
$this->assertSame( 'rw-plain-key', $support->get_api_key() );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user