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
185 lines
6.2 KiB
PHP
185 lines
6.2 KiB
PHP
<?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() );
|
|
}
|
|
}
|