PR Vision v0.2.0: admin/settings interface + correctness fixes
Requirements 1-8 + all non-negotiables:
1. Model manager (CRUD): PRV_Model_Registry, prv_models v2 option, versioned
idempotent migration from v0.1.x flat-string format, Settings page CRUD.
2. Per-model run-health: PRV_Model_Registry::update_health() after each run;
health badges (Healthy/Retired?/Disabled/Unknown) in model table + dashboard.
3. Metric comparability: PRV_Config_Version stamps every run with config_version;
save-bar warning on scoring-relevant changes; orphaned rows excluded by version.
4. Projected cost at save: PRV_Config::get_projected_cost(); over-cap warning;
prv_last_run_truncated option + visible notice when cap hit mid-run.
5. Config surface: peptide set, intents, cadence (reschedules cron), budget cap;
all validated + sanitized; next-run timestamp shown.
6. API-key three-state: not-defined / defined-ok / defined-failed; derived from
ledger; never displays the key.
7. Run-lock: PRV_Run_Lock (transient); cron tick skips if locked; Run-now
refuses with clear message; lock released in finally block.
8. Design fixes: dark-ink focus ring on bright fills; disabled-row full AA
contrast (no opacity); toggle disabled on retired rows; save-bar-level
warning (role=alert); chip text #B4BDC7 6.83:1 AA; keyboard-sortable headers
(<button>+aria-sort); aria-live on Test chip; z-index:50 save-bar;
run-now double-fire guard; @media print light overrides.
New: PRV_Upgrader (runs migrations on plugins_loaded, not just activation),
PRV_Config_Version, PRV_Run_Lock, PRV_Settings_Page, PRV_Settings_Renderer,
PRV_Model_Manager_Table, PRV_Model_Test_Ajax.
Tests: test-model-registry, test-run-lock, test-config-version,
test-projected-cost, test-cron-reschedule (+ updated probe-runner,
uninstall, bootstrap).
Schema: config_version INT column added via dbDelta (idempotent).
Uninstall: purges all v0.2 prv_* options + transients.
This commit is contained in:
150
tests/unit/test-model-registry.php
Normal file
150
tests/unit/test-model-registry.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Model_Registry: v1→v2 migration, CRUD, run-health.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Model Registry Tests ===\n";
|
||||
|
||||
// ── Test: migration from v1 flat strings preserves live model set ─────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = [
|
||||
'perplexity/sonar',
|
||||
'openai/gpt-4o-search-preview',
|
||||
'google/gemini-2.0-flash-001',
|
||||
];
|
||||
// No schema version set -- this is the v0.1.x state.
|
||||
|
||||
$migrated = PRV_Model_Registry::run_migration_v2();
|
||||
prv_assert( $migrated, 'v1->v2 migration: run_migration_v2 returns true when migration performed' );
|
||||
|
||||
$models = PRV_Model_Registry::get_all();
|
||||
prv_assert_equals( 3, count( $models ), 'v1->v2 migration: 3 models preserved' );
|
||||
|
||||
$slugs = array_column( $models, 'slug' );
|
||||
prv_assert( in_array( 'perplexity/sonar', $slugs, true ), 'v1->v2 migration: perplexity/sonar preserved' );
|
||||
prv_assert( in_array( 'openai/gpt-4o-search-preview', $slugs, true ), 'v1->v2 migration: gpt-4o preserved' );
|
||||
prv_assert( in_array( 'google/gemini-2.0-flash-001', $slugs, true ), 'v1->v2 migration: gemini preserved' );
|
||||
|
||||
// Each migrated model has the required v2 fields.
|
||||
foreach ( $models as $m ) {
|
||||
prv_assert( isset( $m['id'] ), "v1->v2 migration: model {$m['slug']} has id" );
|
||||
prv_assert( isset( $m['provider'] ), "v1->v2 migration: model {$m['slug']} has provider" );
|
||||
prv_assert( isset( $m['health_status'] ), "v1->v2 migration: model {$m['slug']} has health_status" );
|
||||
prv_assert( true === $m['enabled'], "v1->v2 migration: model {$m['slug']} is enabled by default" );
|
||||
}
|
||||
|
||||
// Schema version updated.
|
||||
prv_assert_equals(
|
||||
PRV_Model_Registry::SCHEMA_VERSION,
|
||||
(int) $GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ],
|
||||
'v1->v2 migration: schema version option updated'
|
||||
);
|
||||
|
||||
// ── Test: migration is idempotent (runs twice, second is no-op) ──────────
|
||||
|
||||
$before_count = count( PRV_Model_Registry::get_all() );
|
||||
$second_run = PRV_Model_Registry::run_migration_v2();
|
||||
prv_assert( false === $second_run, 'idempotent: second call to run_migration_v2 returns false' );
|
||||
prv_assert_equals( $before_count, count( PRV_Model_Registry::get_all() ), 'idempotent: model count unchanged after second migration' );
|
||||
|
||||
// ── Test: upgrade-from-nothing (no prv_models option) seeds defaults ─────
|
||||
|
||||
prv_test_reset();
|
||||
$migrated2 = PRV_Model_Registry::run_migration_v2();
|
||||
prv_assert( $migrated2, 'default seed: migration returns true when no models exist' );
|
||||
$models2 = PRV_Model_Registry::get_all();
|
||||
prv_assert( count( $models2 ) >= 3, 'default seed: at least 3 default models seeded' );
|
||||
|
||||
// ── Test: add model ──────────────────────────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$base_count = count( PRV_Model_Registry::get_all() );
|
||||
$new_id = PRV_Model_Registry::add( 'anthropic/claude-3-haiku', 'openrouter', true, 'Test model' );
|
||||
prv_assert( '' !== $new_id, 'add: returns non-empty id' );
|
||||
prv_assert( str_starts_with( $new_id, 'mdl_' ), 'add: id has mdl_ prefix' );
|
||||
prv_assert_equals( $base_count + 1, count( PRV_Model_Registry::get_all() ), 'add: model count increments' );
|
||||
|
||||
$found = PRV_Model_Registry::find_by_id( $new_id );
|
||||
prv_assert( null !== $found, 'add: find_by_id returns the new model' );
|
||||
prv_assert_equals( 'anthropic/claude-3-haiku', $found['slug'], 'add: slug stored correctly' );
|
||||
prv_assert_equals( 'Test model', $found['note'], 'add: note stored correctly' );
|
||||
|
||||
// ── Test: update model ───────────────────────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$all = PRV_Model_Registry::get_all();
|
||||
$target = $all[0];
|
||||
$result = PRV_Model_Registry::update( $target['id'], [ 'enabled' => false, 'note' => 'Disabled for test' ] );
|
||||
prv_assert( $result, 'update: returns true' );
|
||||
$updated = PRV_Model_Registry::find_by_id( $target['id'] );
|
||||
prv_assert( null !== $updated, 'update: model still exists' );
|
||||
prv_assert( false === $updated['enabled'], 'update: enabled flag updated' );
|
||||
prv_assert_equals( 'Disabled for test', $updated['note'], 'update: note updated' );
|
||||
|
||||
// ── Test: remove model ───────────────────────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$id_to_remove = PRV_Model_Registry::get_all()[0]['id'];
|
||||
$count_before = count( PRV_Model_Registry::get_all() );
|
||||
$removed = PRV_Model_Registry::remove( $id_to_remove );
|
||||
prv_assert( $removed, 'remove: returns true' );
|
||||
prv_assert_equals( $count_before - 1, count( PRV_Model_Registry::get_all() ), 'remove: count decrements' );
|
||||
prv_assert( null === PRV_Model_Registry::find_by_id( $id_to_remove ), 'remove: model gone from get_all' );
|
||||
|
||||
// ── Test: remove non-existent id returns false ───────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
prv_assert( false === PRV_Model_Registry::remove( 'mdl_doesnotexist' ), 'remove: returns false for unknown id' );
|
||||
|
||||
// ── Test: get_enabled_slugs only returns enabled ─────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$all = PRV_Model_Registry::get_all();
|
||||
// Disable first model.
|
||||
PRV_Model_Registry::update( $all[0]['id'], [ 'enabled' => false ] );
|
||||
$slugs = PRV_Model_Registry::get_enabled_slugs();
|
||||
prv_assert( ! in_array( $all[0]['slug'], $slugs, true ), 'get_enabled_slugs: disabled model excluded' );
|
||||
prv_assert_equals( count( $all ) - 1, count( $slugs ), 'get_enabled_slugs: correct count' );
|
||||
|
||||
// ── Test: update_health sets correct status ──────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$all = PRV_Model_Registry::get_all();
|
||||
$slug0 = $all[0]['slug'];
|
||||
$slug1 = isset( $all[1] ) ? $all[1]['slug'] : null;
|
||||
$run_id = 'test-run-' . uniqid( '', true );
|
||||
|
||||
$outcomes = [ $slug0 => [ 'probed' => 5, 'errors' => 0 ] ];
|
||||
if ( $slug1 ) {
|
||||
$outcomes[ $slug1 ] = [ 'probed' => 0, 'errors' => 3 ];
|
||||
}
|
||||
|
||||
PRV_Model_Registry::update_health( $run_id, $outcomes );
|
||||
|
||||
$updated_all = PRV_Model_Registry::get_all();
|
||||
foreach ( $updated_all as $m ) {
|
||||
if ( $m['slug'] === $slug0 ) {
|
||||
prv_assert_equals( 'healthy', $m['health_status'], 'update_health: probed>0,errors=0 => healthy' );
|
||||
prv_assert_equals( 5, $m['health_probed'], 'update_health: health_probed set correctly' );
|
||||
prv_assert_equals( $run_id, $m['health_run_id'], 'update_health: run_id stored' );
|
||||
}
|
||||
if ( $slug1 && $m['slug'] === $slug1 ) {
|
||||
prv_assert_equals( 'retired', $m['health_status'], 'update_health: probed=0,errors>0 => retired' );
|
||||
prv_assert_equals( 3, $m['health_errors'], 'update_health: health_errors set correctly' );
|
||||
}
|
||||
}
|
||||
|
||||
exit( prv_test_summary() );
|
||||
Reference in New Issue
Block a user