cf-removal: add v0.10.0 migration + tests
New class PRAutoBlogger_Migrate_Remove_Cloudflare_V0100 (own file so class-activator.php stays under 300 lines). Runs from PRAutoBlogger::maybe_run_db_migrations() right after the v0.9.0 migration. Idempotent via prautoblogger_migrated_remove_cloudflare_v0100 flag option (picked up by the uninstall prefix sweep). Migrates any user on a Cloudflare image provider to Runware FLUX.1 schnell (closest equivalent, ~bash.0006/image). Detects the three shapes a legacy user can be in: registry-derived provider === 'cloudflare', image_provider option === 'cloudflare', or the legacy slug 'flux-1-schnell'. Cleans up three now-dead options: prautoblogger_cloudflare_ai_token, prautoblogger_cloudflare_account_id, prautoblogger_cf_image_via_gateway. Does NOT touch prautoblogger_ai_gateway_base_url — that option is shared with the OpenRouter AI Gateway route and must stay (live- verified test). Tests (tests/unit/Core/ActivatorRemoveCloudflareTest.php): - cloudflare user → runware schnell - non-cloudflare user preserved - runware user preserved - legacy 'flux-1-schnell' slug triggers migration - second run is a no-op (does not clobber admin manual changes) - AI Gateway URL preserved across every path ImageModelRegistryTest updated: allowed-provider set is now ['runware','openrouter']; new test_cloudflare_entries_removed_v0100 test pins the scope boundary.
This commit is contained in:
99
includes/class-migrate-remove-cloudflare-v0100.php
Normal file
99
includes/class-migrate-remove-cloudflare-v0100.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-shot v0.10.0 migration — remove Cloudflare Workers AI as image provider.
|
||||
*
|
||||
* Cloudflare Workers AI was removed as an image provider in v0.10.0.
|
||||
* Runware FLUX.1 schnell is 65× cheaper at equivalent quality, so any user
|
||||
* still on a `cloudflare` provider must be migrated off — otherwise the
|
||||
* image pipeline throws "no provider for id cloudflare" at the first
|
||||
* generation run after upgrade.
|
||||
*
|
||||
* Split out of class-activator.php so that file stays under the 300-line
|
||||
* cap. The activator's maybe_run_db_migrations() delegates here.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger::maybe_run_db_migrations() once per admin_init
|
||||
* until the `prautoblogger_migrated_remove_cloudflare_v0100`
|
||||
* flag is set.
|
||||
* Dependencies: PRAutoBlogger_Image_Model_Registry::provider_for() for
|
||||
* current-model provider derivation.
|
||||
*
|
||||
* @see class-activator.php — Parallel v0.9.0 migration.
|
||||
* @see class-prautoblogger.php — Calls run() during admin_init.
|
||||
* @see class-image-model-registry.php — Provider derivation source.
|
||||
* @see convo/prautoblogger/threads/2026-04-cloudflare-workers-ai-removal/
|
||||
*/
|
||||
class PRAutoBlogger_Migrate_Remove_Cloudflare_V0100 {
|
||||
|
||||
/**
|
||||
* Migration flag option key. Presence = already migrated.
|
||||
*/
|
||||
private const FLAG = 'prautoblogger_migrated_remove_cloudflare_v0100';
|
||||
|
||||
/**
|
||||
* Run the migration. Idempotent — second + later calls are no-ops.
|
||||
*
|
||||
* Side effects: may update `prautoblogger_image_model` +
|
||||
* `prautoblogger_image_provider`; always deletes
|
||||
* `prautoblogger_cloudflare_ai_token`,
|
||||
* `prautoblogger_cloudflare_account_id`,
|
||||
* `prautoblogger_cf_image_via_gateway`; sets the
|
||||
* migration flag option.
|
||||
*
|
||||
* Explicitly does NOT touch `prautoblogger_ai_gateway_base_url` —
|
||||
* that option is shared with the OpenRouter AI Gateway route and
|
||||
* must stay intact.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run(): void {
|
||||
if ( get_option( self::FLAG ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self::is_user_on_cloudflare() ) {
|
||||
// Closest equivalent — FLUX.1 schnell, Runware-hosted, ~$0.0006/image.
|
||||
update_option( 'prautoblogger_image_model', 'runware:100@1' );
|
||||
update_option( 'prautoblogger_image_provider', 'runware' );
|
||||
}
|
||||
|
||||
// Clean up orphaned CF Workers AI credentials + the dead gateway toggle.
|
||||
delete_option( 'prautoblogger_cloudflare_ai_token' );
|
||||
delete_option( 'prautoblogger_cloudflare_account_id' );
|
||||
delete_option( 'prautoblogger_cf_image_via_gateway' );
|
||||
|
||||
update_option( self::FLAG, '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect whether the site is currently configured to use Cloudflare
|
||||
* Workers AI for image generation.
|
||||
*
|
||||
* Because the v0.10.0 image-model registry no longer contains
|
||||
* Cloudflare entries, `provider_for()` returns '' for legacy
|
||||
* `flux-1-schnell` slugs. We therefore look at three signals and
|
||||
* treat any of them as "on Cloudflare":
|
||||
* 1. Current registry-derived provider === 'cloudflare'.
|
||||
* 2. The legacy `prautoblogger_image_provider` option is 'cloudflare'.
|
||||
* 3. The legacy CF-specific slug `flux-1-schnell` is still persisted
|
||||
* as the image model (pre-registry-rename installs).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_user_on_cloudflare(): bool {
|
||||
$curr = (string) get_option( 'prautoblogger_image_model', '' );
|
||||
$curr_provider = PRAutoBlogger_Image_Model_Registry::provider_for( $curr );
|
||||
|
||||
if ( 'cloudflare' === $curr_provider ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$legacy_provider = (string) get_option( 'prautoblogger_image_provider', '' );
|
||||
if ( 'cloudflare' === $legacy_provider ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 'flux-1-schnell' === $curr;
|
||||
}
|
||||
}
|
||||
@@ -220,7 +220,6 @@ class PRAutoBlogger {
|
||||
}
|
||||
update_option( 'prautoblogger_migrated_style_suffix_v3', '1' );
|
||||
}
|
||||
|
||||
// One-time migration (v4): remove caption-in-image instruction from style suffix.
|
||||
// Captions are now inserted as HTML below the image, not baked into the image.
|
||||
if ( ! get_option( 'prautoblogger_migrated_style_suffix_v4' ) ) {
|
||||
@@ -232,8 +231,9 @@ class PRAutoBlogger {
|
||||
update_option( 'prautoblogger_migrated_style_suffix_v4', '1' );
|
||||
}
|
||||
|
||||
// v0.9.0: flip legacy default image model to Runware schnell.
|
||||
// v0.9.0 — Runware as default image model. v0.10.0 — remove CF Workers AI.
|
||||
PRAutoBlogger_Activator::migrate_default_image_v090();
|
||||
PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
// One-time migration: re-wrap existing encrypted values with "enc:" prefix.
|
||||
if ( ! get_option( 'prautoblogger_migrated_enc_prefix' ) ) {
|
||||
|
||||
@@ -54,12 +54,8 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate the image provider based on the admin setting.
|
||||
*
|
||||
* Returns Runware by default when the setting is unrecognised —
|
||||
* Cloudflare Workers AI was removed as a provider in v0.10.0; legacy
|
||||
* 'cloudflare' values are migrated at admin_init, but an unmigrated
|
||||
* install should still get a working fallback rather than a fatal.
|
||||
* Instantiate the image provider based on the admin setting. Defaults
|
||||
* to Runware on unrecognised values (CF was removed in v0.10.0).
|
||||
*
|
||||
* @return PRAutoBlogger_Image_Provider_Interface
|
||||
*/
|
||||
|
||||
163
tests/unit/Core/ActivatorRemoveCloudflareTest.php
Normal file
163
tests/unit/Core/ActivatorRemoveCloudflareTest.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for the v0.10.0 one-shot migration that removes Cloudflare
|
||||
* Workers AI as an image provider — exercises the cloudflare-user
|
||||
* path, the non-cloudflare-user path, idempotency, and the critical
|
||||
* scope-boundary invariant that `prautoblogger_ai_gateway_base_url`
|
||||
* (shared with the OpenRouter AI Gateway route) is left intact.
|
||||
*
|
||||
* See thread 2026-04-cloudflare-workers-ai-removal/01-cto-handoff.md.
|
||||
*
|
||||
* @package PRAutoBlogger\Tests\Core
|
||||
*/
|
||||
|
||||
namespace PRAutoBlogger\Tests\Core;
|
||||
|
||||
use PRAutoBlogger\Tests\BaseTestCase;
|
||||
use Brain\Monkey\Functions;
|
||||
|
||||
class ActivatorRemoveCloudflareTest extends BaseTestCase {
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $options = [];
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->options = [];
|
||||
|
||||
Functions\when( 'get_option' )->alias(
|
||||
fn( $key, $default = false ) => $this->options[ $key ] ?? $default
|
||||
);
|
||||
Functions\when( 'update_option' )->alias(
|
||||
function ( $key, $value ) {
|
||||
$this->options[ $key ] = $value;
|
||||
return true;
|
||||
}
|
||||
);
|
||||
Functions\when( 'delete_option' )->alias(
|
||||
function ( $key ) {
|
||||
unset( $this->options[ $key ] );
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloudflare user: flipped to Runware schnell, CF credentials removed,
|
||||
* gateway URL preserved, flag set.
|
||||
*/
|
||||
public function test_cloudflare_user_migrated_to_runware(): void {
|
||||
$this->options = [
|
||||
'prautoblogger_image_provider' => 'cloudflare',
|
||||
'prautoblogger_image_model' => 'flux-1-schnell',
|
||||
'prautoblogger_cloudflare_ai_token' => 'enc:SECRET',
|
||||
'prautoblogger_cloudflare_account_id' => 'acct-123',
|
||||
'prautoblogger_cf_image_via_gateway' => '1',
|
||||
// Shared with OpenRouter AI Gateway — MUST be preserved.
|
||||
'prautoblogger_ai_gateway_base_url' => 'https://gateway.ai.cloudflare.com/v1/acct/gw/openrouter',
|
||||
];
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
$this->assertSame( 'runware', $this->options['prautoblogger_image_provider'] );
|
||||
$this->assertSame( 'runware:100@1', $this->options['prautoblogger_image_model'] );
|
||||
|
||||
$this->assertArrayNotHasKey( 'prautoblogger_cloudflare_ai_token', $this->options );
|
||||
$this->assertArrayNotHasKey( 'prautoblogger_cloudflare_account_id', $this->options );
|
||||
$this->assertArrayNotHasKey( 'prautoblogger_cf_image_via_gateway', $this->options );
|
||||
|
||||
$this->assertSame(
|
||||
'https://gateway.ai.cloudflare.com/v1/acct/gw/openrouter',
|
||||
$this->options['prautoblogger_ai_gateway_base_url'],
|
||||
'AI Gateway URL (shared with OpenRouter route) must NOT be touched.'
|
||||
);
|
||||
|
||||
$this->assertSame( '1', $this->options['prautoblogger_migrated_remove_cloudflare_v0100'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-Cloudflare user (e.g. already on OpenRouter Gemini): model +
|
||||
* provider untouched; CF credentials still swept; flag set.
|
||||
*/
|
||||
public function test_non_cloudflare_user_preserved(): void {
|
||||
$this->options = [
|
||||
'prautoblogger_image_provider' => 'openrouter',
|
||||
'prautoblogger_image_model' => 'google/gemini-2.5-flash-image',
|
||||
// Hypothetical orphans from an earlier experiment.
|
||||
'prautoblogger_cloudflare_ai_token' => 'enc:OLD',
|
||||
'prautoblogger_cloudflare_account_id' => 'acct-999',
|
||||
'prautoblogger_ai_gateway_base_url' => 'https://gateway.ai.cloudflare.com/v1/acct/gw/openrouter',
|
||||
];
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
$this->assertSame( 'openrouter', $this->options['prautoblogger_image_provider'] );
|
||||
$this->assertSame( 'google/gemini-2.5-flash-image', $this->options['prautoblogger_image_model'] );
|
||||
|
||||
$this->assertArrayNotHasKey( 'prautoblogger_cloudflare_ai_token', $this->options );
|
||||
$this->assertArrayNotHasKey( 'prautoblogger_cloudflare_account_id', $this->options );
|
||||
$this->assertSame(
|
||||
'https://gateway.ai.cloudflare.com/v1/acct/gw/openrouter',
|
||||
$this->options['prautoblogger_ai_gateway_base_url']
|
||||
);
|
||||
$this->assertSame( '1', $this->options['prautoblogger_migrated_remove_cloudflare_v0100'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Runware user: provider + model untouched; flag set.
|
||||
*/
|
||||
public function test_runware_user_preserved(): void {
|
||||
$this->options = [
|
||||
'prautoblogger_image_provider' => 'runware',
|
||||
'prautoblogger_image_model' => 'runware:101@1',
|
||||
];
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
$this->assertSame( 'runware', $this->options['prautoblogger_image_provider'] );
|
||||
$this->assertSame( 'runware:101@1', $this->options['prautoblogger_image_model'] );
|
||||
$this->assertSame( '1', $this->options['prautoblogger_migrated_remove_cloudflare_v0100'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy slug detection — the pre-registry `flux-1-schnell` model id
|
||||
* still gets migrated even when the provider option is empty.
|
||||
*/
|
||||
public function test_legacy_slug_triggers_migration(): void {
|
||||
$this->options = [
|
||||
'prautoblogger_image_provider' => '', // Unset before v0.9.0 landed.
|
||||
'prautoblogger_image_model' => 'flux-1-schnell',
|
||||
];
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
$this->assertSame( 'runware', $this->options['prautoblogger_image_provider'] );
|
||||
$this->assertSame( 'runware:100@1', $this->options['prautoblogger_image_model'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Running the migration twice is idempotent — the second pass must
|
||||
* return immediately and must not clobber a user change made after
|
||||
* the first migration run.
|
||||
*/
|
||||
public function test_idempotent_second_run_is_noop(): void {
|
||||
$this->options = [
|
||||
'prautoblogger_image_provider' => 'cloudflare',
|
||||
'prautoblogger_image_model' => 'flux-1-schnell',
|
||||
];
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
$this->assertSame( 'runware', $this->options['prautoblogger_image_provider'] );
|
||||
|
||||
// Simulate admin manually switching to OpenRouter after migration.
|
||||
$this->options['prautoblogger_image_provider'] = 'openrouter';
|
||||
$this->options['prautoblogger_image_model'] = 'google/gemini-3.1-flash-image-preview';
|
||||
|
||||
\PRAutoBlogger_Migrate_Remove_Cloudflare_V0100::run();
|
||||
|
||||
// Second run must NOT overwrite the admin's manual choice.
|
||||
$this->assertSame( 'openrouter', $this->options['prautoblogger_image_provider'] );
|
||||
$this->assertSame( 'google/gemini-3.1-flash-image-preview', $this->options['prautoblogger_image_model'] );
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ class ImageModelRegistryTest extends BaseTestCase {
|
||||
|
||||
/**
|
||||
* Every entry returned by get_models() must carry a non-empty id and
|
||||
* a provider of 'runware', 'openrouter', or 'cloudflare'. If this breaks, the
|
||||
* a provider of 'runware' or 'openrouter' (Cloudflare Workers AI was
|
||||
* removed as an image provider in v0.10.0). If this breaks, the
|
||||
* admin save flow will silently drop the provider derivation.
|
||||
*/
|
||||
public function test_every_registry_entry_has_id_and_known_provider(): void {
|
||||
@@ -31,7 +32,7 @@ class ImageModelRegistryTest extends BaseTestCase {
|
||||
$this->assertArrayHasKey( 'provider', $model );
|
||||
$this->assertContains(
|
||||
$model['provider'],
|
||||
[ 'runware', 'openrouter', 'cloudflare' ],
|
||||
[ 'runware', 'openrouter' ],
|
||||
sprintf( 'Unexpected provider %s for model %s', (string) $model['provider'], (string) $model['id'] )
|
||||
);
|
||||
}
|
||||
@@ -42,14 +43,30 @@ class ImageModelRegistryTest extends BaseTestCase {
|
||||
* This is the core of the v0.8.0 collapsed-dropdown contract.
|
||||
*/
|
||||
public function test_provider_for_returns_paired_provider_for_known_model(): void {
|
||||
$this->assertSame(
|
||||
'cloudflare',
|
||||
\PRAutoBlogger_Image_Model_Registry::provider_for( 'flux-1-schnell' )
|
||||
);
|
||||
$this->assertSame(
|
||||
'openrouter',
|
||||
\PRAutoBlogger_Image_Model_Registry::provider_for( 'google/gemini-2.5-flash-image' )
|
||||
);
|
||||
$this->assertSame(
|
||||
'runware',
|
||||
\PRAutoBlogger_Image_Model_Registry::provider_for( 'runware:100@1' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.10.0 scope boundary: no Cloudflare entries remain in the registry.
|
||||
* The legacy `flux-1-schnell` slug (pre-Runware) must resolve to '' so
|
||||
* the migration's detection path catches it. If a registry regression
|
||||
* re-adds a cloudflare entry this test fails loudly.
|
||||
*/
|
||||
public function test_cloudflare_entries_removed_v0100(): void {
|
||||
foreach ( \PRAutoBlogger_Image_Model_Registry::get_models() as $model ) {
|
||||
$this->assertNotSame( 'cloudflare', $model['provider'] ?? '' );
|
||||
}
|
||||
$this->assertSame(
|
||||
'',
|
||||
\PRAutoBlogger_Image_Model_Registry::provider_for( 'flux-1-schnell' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user