feat(images): collapse Image Provider + Image Model into a single dropdown

Until now, Image Provider and Image Model were two independent selects
in the admin UI. That let the CEO set Model=flux-1-schnell while
Provider was still openrouter — OpenRouter has no such model so every
image gen on posts 650/657 on 2026-04-20 returned HTTP 400, and the
article published without a featured image.

v0.8.0 removes the Image Provider field from the admin UI entirely.
The provider is now derived from the model's entry in the registry
every time the model is saved. A new
PRAutoBlogger_Image_Model_Registry::provider_for() helper centralises
the mapping, so admin save, the on-upgrade migration, and future
callers all read from one place.

On save, PRAutoBlogger_Admin_Page::sanitize_image_model() validates
the submitted slug against the registry:
- Known slug: persist it, and update `prautoblogger_image_provider` to
  the paired provider as a side-effect.
- Unknown slug: add_settings_error with a clear message, and return
  the previously-saved value so the site stays on a working model.

A one-shot v0.8.0 migration in
PRAutoBlogger::on_check_db_version() derives the provider from the
currently-saved model on first admin_init after the plugin is updated.
This auto-heals sites that were running mismatched pairs before the
upgrade, without requiring the admin to re-open settings.

`prautoblogger_image_provider` option key is preserved — all existing
reads in the pipeline, cost tracker, and validators continue to work
unchanged.

Tests (Services/ImageModelRegistryTest):

- Every registry entry has an id and a provider in {openrouter, cloudflare}.
- provider_for() returns the paired provider for a known slug.
- provider_for() returns '' for unknown/empty slugs.

Cost impact: none.

Migration idempotent via the `prautoblogger_migrated_image_provider_v080`
flag. Uninstall sweeps it along with every other plugin option.

Part of the v0.8.0 image bundle (thread image-mime-bug, Issue 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-21 01:16:30 +07:00
parent 0505e50e85
commit 52a8ffbd9e
5 changed files with 115 additions and 14 deletions

View File

@@ -0,0 +1,69 @@
<?php
/**
* Tests for PRAutoBlogger_Image_Model_Registry.
*
* The admin save flow (v0.8.0) collapses Provider and Image Model into a
* single dropdown and derives the provider from the model via
* Image_Model_Registry::provider_for(). This test pins that mapping down
* so a registry edit that breaks the contract fails CI.
*
* @package PRAutoBlogger\Tests\Services
*/
namespace PRAutoBlogger\Tests\Services;
use PRAutoBlogger\Tests\BaseTestCase;
class ImageModelRegistryTest extends BaseTestCase {
/**
* Every entry returned by get_models() must carry a non-empty id and
* a provider of 'openrouter' or 'cloudflare'. 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 {
$models = \PRAutoBlogger_Image_Model_Registry::get_models();
$this->assertNotEmpty( $models );
foreach ( $models as $model ) {
$this->assertArrayHasKey( 'id', $model );
$this->assertNotEmpty( $model['id'] );
$this->assertArrayHasKey( 'provider', $model );
$this->assertContains(
$model['provider'],
[ 'openrouter', 'cloudflare' ],
sprintf( 'Unexpected provider %s for model %s', (string) $model['provider'], (string) $model['id'] )
);
}
}
/**
* provider_for() returns the paired provider for a known model id.
* 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' )
);
}
/**
* Unknown model ids return an empty string so callers can distinguish
* "not in registry" from a valid provider and show a settings error.
*/
public function test_provider_for_returns_empty_for_unknown_model(): void {
$this->assertSame(
'',
\PRAutoBlogger_Image_Model_Registry::provider_for( 'nonexistent/model' )
);
$this->assertSame(
'',
\PRAutoBlogger_Image_Model_Registry::provider_for( '' )
);
}
}