PR Vision v0.2.0: admin/settings interface + correctness fixes (#3)
Model manager (UI-swappable models) + versioned/idempotent prv_models migration; metric comparability (config-version stamping); run-lock; projected cost + truncation state; per-model run-health; cadence reschedule; API-key 3-state; design/a11y fixes (dark v2). QA: APPROVE-WITH-P2-NOTES — qa-reviews/pr-vision/2026-06-14-ee4371d9163f992f60e09c7e4ac6cf8cd1d3c5eb.md Agent-Session: pr-vision admin v0.2.0 merge
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal test bootstrap — stubs all WordPress functions used by the plugin
|
||||
* Minimal test bootstrap -- stubs all WordPress functions used by the plugin
|
||||
* so classes can be exercised in plain PHP without a WP install.
|
||||
*
|
||||
* Pattern mirrors peptide-repo-core's bootstrap (flat PHP, no PHPUnit).
|
||||
@@ -16,11 +16,11 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
define( 'ABSPATH', __DIR__ . '/' );
|
||||
}
|
||||
|
||||
define( 'PRV_VERSION', '0.1.1' );
|
||||
define( 'PRV_VERSION', '0.2.0' );
|
||||
define( 'PRV_PLUGIN_FILE', __DIR__ . '/../pr-vision.php' );
|
||||
define( 'PRV_PLUGIN_DIR', realpath( __DIR__ . '/..' ) . '/' );
|
||||
define( 'PRV_PLUGIN_URL', 'http://example.test/wp-content/plugins/pr-vision/' );
|
||||
define( 'PRV_SCHEMA_VERSION', 1 );
|
||||
define( 'PRV_SCHEMA_VERSION', 2 );
|
||||
define( 'PRV_MAX_RETRIES', 3 );
|
||||
define( 'PRV_API_TIMEOUT_SECONDS', 60 );
|
||||
define( 'PRV_RETRY_BASE_DELAY_SECONDS', 2 );
|
||||
@@ -32,6 +32,7 @@ define( 'PRV_TARGET_DOMAIN', 'peptiderepo.com' );
|
||||
|
||||
$GLOBALS['prv_test_state'] = [
|
||||
'options' => [],
|
||||
'transients' => [],
|
||||
'actions' => [],
|
||||
'wpdb_insert_id' => 1,
|
||||
'wpdb_results' => [],
|
||||
@@ -43,6 +44,7 @@ $GLOBALS['prv_test_state'] = [
|
||||
function prv_test_reset(): void {
|
||||
$GLOBALS['prv_test_state'] = [
|
||||
'options' => [],
|
||||
'transients' => [],
|
||||
'actions' => [],
|
||||
'wpdb_insert_id' => 1,
|
||||
'wpdb_results' => [],
|
||||
@@ -71,23 +73,41 @@ function add_option( string $name, $value = '' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
function delete_option( string $name ): bool {
|
||||
unset( $GLOBALS['prv_test_state']['options'][ $name ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_transient( string $name ) {
|
||||
return $GLOBALS['prv_test_state']['transients'][ $name ] ?? false;
|
||||
}
|
||||
|
||||
function set_transient( string $name, $value, int $ttl = 0 ): bool {
|
||||
$GLOBALS['prv_test_state']['transients'][ $name ] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
function delete_transient( string $name ): bool {
|
||||
unset( $GLOBALS['prv_test_state']['transients'][ $name ] );
|
||||
return true;
|
||||
}
|
||||
|
||||
function add_action( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
|
||||
$GLOBALS['prv_test_state']['actions'][] = compact( 'hook', 'callback', 'priority' );
|
||||
return true;
|
||||
}
|
||||
|
||||
function remove_action( string $hook, $callback, int $priority = 10 ): bool {
|
||||
return true;
|
||||
}
|
||||
function remove_action( string $hook, $callback, int $priority = 10 ): bool { return true; }
|
||||
|
||||
function add_menu_page( ...$args ): void {}
|
||||
function add_submenu_page( ...$args ): void {}
|
||||
|
||||
function wp_schedule_event( int $timestamp, string $recurrence, string $hook ): void {
|
||||
$GLOBALS['prv_test_state']['cron_events'][ $hook ] = $timestamp;
|
||||
$GLOBALS['prv_test_state']['cron_events'][ $hook ] = [ 'timestamp' => $timestamp, 'schedule' => $recurrence ];
|
||||
}
|
||||
|
||||
function wp_next_scheduled( string $hook ) {
|
||||
return $GLOBALS['prv_test_state']['cron_events'][ $hook ] ?? false;
|
||||
return $GLOBALS['prv_test_state']['cron_events'][ $hook ]['timestamp'] ?? false;
|
||||
}
|
||||
|
||||
function wp_unschedule_event( int $timestamp, string $hook ): void {
|
||||
@@ -98,6 +118,14 @@ function wp_clear_scheduled_hook( string $hook ): void {
|
||||
unset( $GLOBALS['prv_test_state']['cron_events'][ $hook ] );
|
||||
}
|
||||
|
||||
function _get_cron_array(): array {
|
||||
$out = [];
|
||||
foreach ( $GLOBALS['prv_test_state']['cron_events'] as $hook => $data ) {
|
||||
$out[ $data['timestamp'] ][ $hook ] = [ '' => [ 'schedule' => $data['schedule'], 'interval' => 604800 ] ];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function wp_rand( int $min = 0, int $max = 0 ): int {
|
||||
return random_int( $min ?: 0, $max ?: PHP_INT_MAX );
|
||||
}
|
||||
@@ -106,29 +134,15 @@ function current_time( string $type, bool $gmt = false ): string {
|
||||
return gmdate( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
function home_url( string $path = '' ): string {
|
||||
return 'http://example.test' . $path;
|
||||
}
|
||||
function home_url( string $path = '' ): string { return 'http://example.test' . $path; }
|
||||
|
||||
function __( string $text, string $domain = 'default' ): string {
|
||||
return $text;
|
||||
}
|
||||
|
||||
function esc_html( string $text ): string {
|
||||
return htmlspecialchars( $text, ENT_QUOTES );
|
||||
}
|
||||
|
||||
function esc_html__( string $text, string $domain = 'default' ): string {
|
||||
return esc_html( $text );
|
||||
}
|
||||
|
||||
function esc_js( string $text ): string {
|
||||
return addslashes( $text );
|
||||
}
|
||||
|
||||
function esc_url( string $url ): string {
|
||||
return $url;
|
||||
}
|
||||
function __( string $text, string $domain = 'default' ): string { return $text; }
|
||||
function esc_html( string $text ): string { return htmlspecialchars( $text, ENT_QUOTES ); }
|
||||
function esc_html__( string $text, string $domain = 'default' ): string { return esc_html( $text ); }
|
||||
function esc_attr( string $text ): string { return htmlspecialchars( $text, ENT_QUOTES ); }
|
||||
function esc_textarea( string $text ): string { return htmlspecialchars( $text, ENT_QUOTES ); }
|
||||
function esc_js( string $text ): string { return addslashes( $text ); }
|
||||
function esc_url( string $url ): string { return $url; }
|
||||
|
||||
function wp_json_encode( $data, int $flags = 0 ): string {
|
||||
return json_encode( $data, $flags ) ?: 'null';
|
||||
@@ -138,66 +152,50 @@ function wp_parse_url( string $url, int $component = -1 ) {
|
||||
return parse_url( $url, $component );
|
||||
}
|
||||
|
||||
function absint( $value ): int {
|
||||
return abs( (int) $value );
|
||||
}
|
||||
|
||||
function sanitize_text_field( string $str ): string {
|
||||
return trim( $str );
|
||||
}
|
||||
|
||||
function current_user_can( string $cap ): bool {
|
||||
return true;
|
||||
}
|
||||
function absint( $value ): int { return abs( (int) $value ); }
|
||||
function sanitize_text_field( string $str ): string { return trim( $str ); }
|
||||
function sanitize_textarea_field( string $str ): string { return trim( $str ); }
|
||||
function wp_unslash( $value ) { return is_string( $value ) ? stripslashes( $value ) : $value; }
|
||||
|
||||
function current_user_can( string $cap ): bool { return true; }
|
||||
function wp_die( $message = '', $code = 0 ): void {
|
||||
throw new \RuntimeException( is_string( $message ) ? $message : 'wp_die' );
|
||||
}
|
||||
|
||||
function wp_verify_nonce( string $nonce, string $action ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
function wp_nonce_field( string $action, string $name ): string {
|
||||
return '';
|
||||
}
|
||||
function wp_verify_nonce( string $nonce, string $action ): bool { return true; }
|
||||
function wp_nonce_field( string $action, string $name, bool $referer = true, bool $echo = true ): string { return ''; }
|
||||
function wp_create_nonce( string $action ): string { return 'test_nonce'; }
|
||||
|
||||
function submit_button( string $text, string $type = 'primary', string $name = 'submit', bool $wrap = true ): void {}
|
||||
|
||||
function add_query_arg( array $args, string $url ): string {
|
||||
return $url . '?' . http_build_query( $args );
|
||||
}
|
||||
|
||||
function admin_url( string $path ): string {
|
||||
return 'http://example.test/wp-admin/' . ltrim( $path, '/' );
|
||||
}
|
||||
|
||||
function add_query_arg( array $args, string $url ): string { return $url . '?' . http_build_query( $args ); }
|
||||
function admin_url( string $path ): string { return 'http://example.test/wp-admin/' . ltrim( $path, '/' ); }
|
||||
function wp_safe_redirect( string $url ): void {}
|
||||
function wp_send_json_success( $data = null ): void { throw new \RuntimeException( 'json_success:' . json_encode( $data ) ); }
|
||||
function wp_send_json_error( $data = null ): void { throw new \RuntimeException( 'json_error:' . json_encode( $data ) ); }
|
||||
|
||||
function is_wp_error( $thing ): bool {
|
||||
return $thing instanceof WP_Error;
|
||||
function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; }
|
||||
|
||||
function selected( $selected, $current = true, bool $echo = true ): string {
|
||||
$str = ( (string) $selected === (string) $current ) ? ' selected="selected"' : '';
|
||||
if ( $echo ) { echo $str; }
|
||||
return $str;
|
||||
}
|
||||
|
||||
function wp_remote_post( string $url, array $args = array() ) {
|
||||
// Return from test queue if set, else a generic success.
|
||||
$queue = &$GLOBALS['prv_test_state']['remote_posts'];
|
||||
if ( ! empty( $queue ) ) {
|
||||
return array_shift( $queue );
|
||||
}
|
||||
if ( ! empty( $queue ) ) { return array_shift( $queue ); }
|
||||
return array( 'response' => array( 'code' => 200 ), 'body' => '{"choices":[{"message":{"content":"test"}}],"citations":[],"usage":{"total_tokens":100}}' );
|
||||
}
|
||||
|
||||
function wp_remote_retrieve_response_code( $response ): int {
|
||||
return (int) ( $response['response']['code'] ?? 200 );
|
||||
}
|
||||
function wp_remote_retrieve_body( $response ): string { return (string) ( $response['body'] ?? '' ); }
|
||||
function wp_remote_retrieve_header( $response, string $header ): string { return (string) ( $response['headers'][ $header ] ?? '' ); }
|
||||
|
||||
function wp_remote_retrieve_body( $response ): string {
|
||||
return (string) ( $response['body'] ?? '' );
|
||||
}
|
||||
|
||||
function wp_remote_retrieve_header( $response, string $header ): string {
|
||||
return (string) ( $response['headers'][ $header ] ?? '' );
|
||||
}
|
||||
function is_admin(): bool { return false; }
|
||||
function wp_enqueue_script( ...$args ): void {}
|
||||
function wp_add_inline_style( ...$args ): void {}
|
||||
|
||||
class WP_Error {
|
||||
public function __construct( private string $code = '', private string $message = '' ) {}
|
||||
@@ -207,10 +205,11 @@ class WP_Error {
|
||||
/* ── Minimal $wpdb stub ────────────────────────────────────────────────── */
|
||||
|
||||
class stdClass_wpdb {
|
||||
public string $prefix = 'wp_';
|
||||
public string $prefix = 'wp_';
|
||||
public int $insert_id = 0;
|
||||
public array $_results = [];
|
||||
public mixed $_var = null;
|
||||
public string $options = 'wp_options';
|
||||
|
||||
public function get_charset_collate(): string {
|
||||
return 'DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci';
|
||||
@@ -223,15 +222,11 @@ class stdClass_wpdb {
|
||||
$this->insert_id = $GLOBALS['prv_test_state']['wpdb_insert_id']++;
|
||||
return 1;
|
||||
}
|
||||
public function update( string $table, array $data, array $where, array $data_format = [], array $where_format = [] ): int {
|
||||
return 1;
|
||||
}
|
||||
public function update( string $table, array $data, array $where, array $df = [], array $wf = [] ): int { return 1; }
|
||||
public function get_results( string $query, string $output = 'OBJECT' ): array {
|
||||
return is_array( $GLOBALS['prv_test_state']['wpdb_results'] ) ? $GLOBALS['prv_test_state']['wpdb_results'] : [];
|
||||
}
|
||||
public function get_var( string $query ): mixed {
|
||||
return $GLOBALS['prv_test_state']['wpdb_var'];
|
||||
}
|
||||
public function get_var( string $query ): mixed { return $GLOBALS['prv_test_state']['wpdb_var']; }
|
||||
}
|
||||
|
||||
$wpdb = new stdClass_wpdb();
|
||||
@@ -255,15 +250,15 @@ function prv_assert( bool $condition, string $label ): void {
|
||||
|
||||
function prv_assert_equals( $expected, $actual, string $label ): void {
|
||||
$pass = ( $expected === $actual );
|
||||
prv_assert( $pass, $label . ( $pass ? '' : ' — expected ' . var_export( $expected, true ) . ', got ' . var_export( $actual, true ) ) );
|
||||
prv_assert( $pass, $label . ( $pass ? '' : ' -- expected ' . var_export( $expected, true ) . ', got ' . var_export( $actual, true ) ) );
|
||||
}
|
||||
|
||||
function prv_assert_throws( callable $fn, string $exception_class, string $label ): void {
|
||||
try {
|
||||
$fn();
|
||||
prv_assert( false, $label . ' — expected ' . $exception_class . ' but no exception thrown' );
|
||||
prv_assert( false, $label . ' -- expected ' . $exception_class . ' but no exception thrown' );
|
||||
} catch ( \Throwable $e ) {
|
||||
prv_assert( $e instanceof $exception_class, $label . ' — got ' . get_class( $e ) );
|
||||
prv_assert( $e instanceof $exception_class, $label . ' -- got ' . get_class( $e ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,9 +268,7 @@ function prv_test_summary(): int {
|
||||
echo "Totals: {$r['pass']} passed, {$r['fail']} failed\n";
|
||||
if ( $r['fail'] > 0 ) {
|
||||
echo "Failures:\n";
|
||||
foreach ( $r['failures'] as $f ) {
|
||||
echo " - {$f}\n";
|
||||
}
|
||||
foreach ( $r['failures'] as $f ) { echo " - {$f}\n"; }
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
|
||||
120
tests/unit/test-config-version.php
Normal file
120
tests/unit/test-config-version.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Config_Version: config hash, bump, version records.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Config Version Tests ===\n";
|
||||
|
||||
// Shared API key constant.
|
||||
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) {
|
||||
define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) {
|
||||
define( 'PRV_CF_ACCOUNT_ID', '' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) {
|
||||
define( 'PRV_CF_GATEWAY_ID', '' );
|
||||
}
|
||||
|
||||
// ── Test: maybe_seed_initial_version creates a v1 record ────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
|
||||
$versions = PRV_Config_Version::get_all_versions();
|
||||
prv_assert( count( $versions ) >= 1, 'seed: at least 1 version record after initial seed' );
|
||||
prv_assert_equals( 1, (int) $versions[0]['version'], 'seed: first version number is 1' );
|
||||
prv_assert( ! empty( $versions[0]['hash'] ), 'seed: version has a hash' );
|
||||
prv_assert_equals( 1, PRV_Config_Version::get_active_version(), 'seed: active version is 1' );
|
||||
|
||||
// ── Test: maybe_seed_initial_version is idempotent ───────────────────────
|
||||
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
prv_assert_equals( 1, count( PRV_Config_Version::get_all_versions() ), 'seed idempotent: still 1 version after second seed call' );
|
||||
|
||||
// ── Test: bump when config unchanged returns null ────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
|
||||
$bump = PRV_Config_Version::bump_version_if_changed();
|
||||
prv_assert( null === $bump, 'bump: returns null when config unchanged' );
|
||||
prv_assert_equals( 1, PRV_Config_Version::get_active_version(), 'bump: active version stays 1 when config unchanged' );
|
||||
|
||||
// ── Test: bump after scoring-relevant change creates new version ─────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
|
||||
// Change the model list (scoring-relevant).
|
||||
$all = PRV_Model_Registry::get_all();
|
||||
PRV_Model_Registry::add( 'anthropic/claude-3-haiku', 'openrouter', true, 'New model' );
|
||||
|
||||
$new_ver = PRV_Config_Version::bump_version_if_changed();
|
||||
prv_assert( null !== $new_ver, 'bump: returns new version number when config changed' );
|
||||
prv_assert_equals( 2, (int) $new_ver, 'bump: new version is 2' );
|
||||
prv_assert_equals( 2, PRV_Config_Version::get_active_version(), 'bump: active version updated to 2' );
|
||||
prv_assert_equals( 2, count( PRV_Config_Version::get_all_versions() ), 'bump: 2 version records exist' );
|
||||
|
||||
// ── Test: would_change detects a config change ───────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
|
||||
$original_hash = PRV_Config_Version::compute_hash();
|
||||
prv_assert( false === PRV_Config_Version::would_change( $original_hash ), 'would_change: false when hash is current' );
|
||||
|
||||
// Modify config.
|
||||
PRV_Model_Registry::add( 'x/new-model', 'openrouter', true );
|
||||
$new_hash = PRV_Config_Version::compute_hash();
|
||||
prv_assert( true === PRV_Config_Version::would_change( $new_hash ), 'would_change: true when config differs' );
|
||||
|
||||
// ── Test: compute_hash is stable for same config ─────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$h1 = PRV_Config_Version::compute_hash();
|
||||
$h2 = PRV_Config_Version::compute_hash();
|
||||
prv_assert_equals( $h1, $h2, 'compute_hash: stable for same config' );
|
||||
|
||||
// ── Test: config_version stamped on probe result ─────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
prv_assert_equals( 1, PRV_Config_Version::get_active_version(), 'version stamp: active version is 1 before run' );
|
||||
|
||||
// Verify the runner writes config_version to DB (check the last DB insert args).
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ [ 'slug' => 'bpc-157', 'label' => 'BPC-157' ] ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}' ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = [
|
||||
[ 'id' => 'mdl_test01', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ]
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ] = PRV_Model_Registry::SCHEMA_VERSION;
|
||||
|
||||
$cited_body = json_encode([
|
||||
'choices' => [['message' => ['content' => 'test']]],
|
||||
'citations' => ['https://peptiderepo.com/bpc-157/'],
|
||||
'usage' => ['total_tokens' => 50],
|
||||
]);
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
|
||||
$runner = new PRV_Probe_Runner( new PRV_Cost_Ledger() );
|
||||
$result = $runner->run();
|
||||
prv_assert_equals( 1, $result['probed'], 'version stamp: runner completes 1 probe' );
|
||||
// The prv_active_config_version persists after the run.
|
||||
prv_assert_equals( 1, PRV_Config_Version::get_active_version(), 'version stamp: config version unchanged after run' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
90
tests/unit/test-cron-reschedule.php
Normal file
90
tests/unit/test-cron-reschedule.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Cron cadence reschedule on config change.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Cron Reschedule Tests ===\n";
|
||||
|
||||
// ── Test: schedule_weekly schedules with the configured cadence ───────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
PRV_Cron::schedule_weekly();
|
||||
prv_assert( PRV_Cron::is_scheduled(), 'schedule: event scheduled' );
|
||||
$next = PRV_Cron::next_run_timestamp();
|
||||
prv_assert( false !== $next, 'schedule: next_run_timestamp is not false' );
|
||||
prv_assert( (int) $next > 0, 'schedule: next_run_timestamp is positive' );
|
||||
|
||||
// ── Test: reschedule clears old and adds new event ────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
PRV_Cron::schedule();
|
||||
prv_assert( PRV_Cron::is_scheduled(), 'reschedule pre: event is scheduled' );
|
||||
|
||||
PRV_Cron::reschedule( 'daily' );
|
||||
prv_assert( PRV_Cron::is_scheduled(), 'reschedule post: event is still scheduled after reschedule' );
|
||||
|
||||
// Verify the cron array stores the new recurrence.
|
||||
$events = _get_cron_array();
|
||||
$found_daily = false;
|
||||
foreach ( $events as $hooks ) {
|
||||
if ( isset( $hooks[ PRV_CRON_HOOK ] ) ) {
|
||||
foreach ( $hooks[ PRV_CRON_HOOK ] as $evt ) {
|
||||
if ( 'daily' === ( $evt['schedule'] ?? '' ) ) {
|
||||
$found_daily = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prv_assert( $found_daily, 'reschedule: new cron has daily recurrence' );
|
||||
|
||||
// ── Test: clear_schedule removes the event ────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Cron::schedule();
|
||||
prv_assert( PRV_Cron::is_scheduled(), 'clear pre: event is scheduled' );
|
||||
PRV_Cron::clear_schedule();
|
||||
prv_assert( false === PRV_Cron::is_scheduled(), 'clear: event removed after clear_schedule' );
|
||||
|
||||
// ── Test: schedule is idempotent (called twice) ───────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
PRV_Cron::schedule();
|
||||
$ts1 = PRV_Cron::next_run_timestamp();
|
||||
PRV_Cron::schedule(); // Second call.
|
||||
$ts2 = PRV_Cron::next_run_timestamp();
|
||||
prv_assert( $ts1 === $ts2, 'schedule idempotent: same timestamp after two schedule() calls at same cadence' );
|
||||
|
||||
// ── Test: get_cadence default is weekly ──────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$cadence = PRV_Config::get_cadence();
|
||||
prv_assert_equals( 'weekly', $cadence, 'get_cadence: defaults to weekly' );
|
||||
|
||||
// ── Test: invalid cadence falls back to weekly ────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'invalid_cadence';
|
||||
$cadence = PRV_Config::get_cadence();
|
||||
prv_assert_equals( 'weekly', $cadence, 'get_cadence: invalid value falls back to weekly' );
|
||||
|
||||
// ── Test: cron tick skipped when run-lock held ───────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Run_Lock::acquire();
|
||||
$cron = new PRV_Cron();
|
||||
// handle_cron_tick should return without running -- no exception.
|
||||
$cron->handle_cron_tick();
|
||||
prv_assert( PRV_Run_Lock::is_locked(), 'cron tick: lock still held (run was skipped)' );
|
||||
PRV_Run_Lock::release();
|
||||
prv_assert( false === PRV_Run_Lock::is_locked(), 'cron tick cleanup: lock released' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
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() );
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Probe_Runner: budget-cap abort and run mechanics.
|
||||
* Tests for PRV_Probe_Runner: budget-cap abort, run-lock, model health.
|
||||
*
|
||||
* Uses queued mock HTTP responses (via $GLOBALS['prv_test_state']['remote_posts'])
|
||||
* and a mock ledger to verify runner behaviour without real API calls.
|
||||
@@ -47,7 +47,18 @@ class PRV_Mock_Ledger extends PRV_Cost_Ledger {
|
||||
public function get_month_to_date_usd(): float { return 0.0; }
|
||||
}
|
||||
|
||||
// ── Shared mock response body (Perplexity / cited) ────────────────────────
|
||||
// ── Shared helpers ────────────────────────────────────────────────────────
|
||||
|
||||
function setup_simple_run( array $model_rows, array $peptides = null, array $intents = null ): void {
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = $model_rows;
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ] = PRV_Model_Registry::SCHEMA_VERSION;
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = $peptides ?? [['slug'=>'bpc-157','label'=>'BPC-157']];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = $intents ?? ['what is {peptide}'];
|
||||
}
|
||||
|
||||
$sonar_row = [ 'id' => 'mdl_t1', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ];
|
||||
|
||||
$cited_body = json_encode([
|
||||
'choices' => [['message' => ['content' => 'BPC-157 info.']]],
|
||||
@@ -55,12 +66,10 @@ $cited_body = json_encode([
|
||||
'usage' => ['total_tokens' => 100],
|
||||
]);
|
||||
|
||||
// ── Test: basic run completes — 1×1×1 (peptide × intent × model) ─────────
|
||||
// ── Test: basic run completes -- 1×1×1 ───────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
@@ -73,16 +82,17 @@ prv_assert_equals( 1, $counts1['probed'], 'basic run: 1 probed' );
|
||||
prv_assert_equals( 0, $counts1['skipped_budget'], 'basic run: 0 budget skips' );
|
||||
prv_assert_equals( 0, $counts1['skipped_error'], 'basic run: 0 error skips' );
|
||||
prv_assert_equals( 1, $ledger1->afford_count, 'basic run: 1 can_afford check' );
|
||||
prv_assert( '' !== $counts1['run_id'], 'basic run: run_id is non-empty' );
|
||||
prv_assert( false === $counts1['truncated'], 'basic run: not truncated' );
|
||||
|
||||
// ── Test: 2×1×1 run completes all probes when budget sufficient ───────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [
|
||||
['slug'=>'bpc-157','label'=>'BPC-157'],
|
||||
['slug'=>'tb-500','label'=>'TB-500'],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
setup_simple_run(
|
||||
[$sonar_row],
|
||||
[['slug'=>'bpc-157','label'=>'BPC-157'],['slug'=>'tb-500','label'=>'TB-500']],
|
||||
['what is {peptide}']
|
||||
);
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
@@ -92,43 +102,38 @@ $ledger2 = new PRV_Mock_Ledger();
|
||||
$runner2 = new PRV_Probe_Runner( $ledger2 );
|
||||
$counts2 = $runner2->run();
|
||||
|
||||
prv_assert_equals( 2, $counts2['probed'], '2×1×1 run: 2 probes completed' );
|
||||
prv_assert_equals( 2, $ledger2->afford_count, '2×1×1 run: 2 can_afford checks' );
|
||||
prv_assert_equals( 2, $counts2['probed'], '2×1×1 run: 2 probes completed' );
|
||||
prv_assert_equals( 2, $ledger2->afford_count, '2×1×1 run: 2 can_afford checks' );
|
||||
|
||||
// ── Test: budget cap abort after N probes ────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [
|
||||
['slug'=>'bpc-157','label'=>'BPC-157'],
|
||||
['slug'=>'tb-500','label'=>'TB-500'],
|
||||
['slug'=>'mk-677','label'=>'MK-677'],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
// Queue 3 responses but cap budget at 1.
|
||||
setup_simple_run(
|
||||
[$sonar_row],
|
||||
[['slug'=>'bpc-157','label'=>'BPC-157'],['slug'=>'tb-500','label'=>'TB-500'],['slug'=>'mk-677','label'=>'MK-677']],
|
||||
['what is {peptide}']
|
||||
);
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
|
||||
$exhausting_ledger = new PRV_Mock_Ledger();
|
||||
$exhausting_ledger->exhaust_after = 1; // Budget exhausted after 1st can_afford check.
|
||||
$exh = new PRV_Mock_Ledger();
|
||||
$exh->exhaust_after = 1; // Budget exhausted after 1st check.
|
||||
|
||||
$runner3 = new PRV_Probe_Runner( $exhausting_ledger );
|
||||
$runner3 = new PRV_Probe_Runner( $exh );
|
||||
$counts3 = $runner3->run();
|
||||
|
||||
prv_assert_equals( 1, $counts3['probed'], 'budget abort: 1 probe completed before cap' );
|
||||
prv_assert( $counts3['skipped_budget'] >= 2, 'budget abort: ≥2 probes skipped (budget)' );
|
||||
prv_assert( $counts3['skipped_budget'] >= 2, 'budget abort: >=2 probes skipped (budget)' );
|
||||
prv_assert_equals( 0, $counts3['skipped_error'], 'budget abort: 0 error skips' );
|
||||
prv_assert( $counts3['truncated'], 'budget abort: truncated flag set' );
|
||||
|
||||
// ── Test: HTTP error counts as skipped_error ─────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
// All retries fail with 500.
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 500], 'body' => 'Error'],
|
||||
['response' => ['code' => 500], 'body' => 'Error'],
|
||||
@@ -140,15 +145,12 @@ $runner4 = new PRV_Probe_Runner( $ledger4 );
|
||||
$counts4 = $runner4->run();
|
||||
|
||||
prv_assert_equals( 0, $counts4['probed'], 'HTTP 500: 0 probes' );
|
||||
prv_assert( $counts4['skipped_error'] >= 1, 'HTTP 500: ≥1 error skip recorded' );
|
||||
prv_assert( $counts4['skipped_error'] >= 1, 'HTTP 500: >=1 error skip recorded' );
|
||||
|
||||
// ── Test: WP_Error from wp_remote_post counts as skipped_error ──────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
// Return WP_Error for all retries.
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$wp_error = new WP_Error( 'http_request_failed', 'cURL error 28: Operation timed out' );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [ $wp_error, $wp_error, $wp_error ];
|
||||
|
||||
@@ -157,14 +159,12 @@ $runner5 = new PRV_Probe_Runner( $ledger5 );
|
||||
$counts5 = $runner5->run();
|
||||
|
||||
prv_assert_equals( 0, $counts5['probed'], 'WP_Error: 0 probes completed' );
|
||||
prv_assert( $counts5['skipped_error'] >= 1, 'WP_Error: ≥1 error skip recorded' );
|
||||
prv_assert( $counts5['skipped_error'] >= 1, 'WP_Error: >=1 error skip recorded' );
|
||||
|
||||
// ── Test: run returns correct keys in counts array ───────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [['slug'=>'bpc-157','label'=>'BPC-157']];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = ['what is {peptide}'];
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = ['perplexity/sonar'];
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
@@ -176,5 +176,53 @@ $counts6 = $runner6->run();
|
||||
prv_assert( array_key_exists( 'probed', $counts6 ), 'run: returns probed key' );
|
||||
prv_assert( array_key_exists( 'skipped_budget', $counts6 ), 'run: returns skipped_budget key' );
|
||||
prv_assert( array_key_exists( 'skipped_error', $counts6 ), 'run: returns skipped_error key' );
|
||||
prv_assert( array_key_exists( 'truncated', $counts6 ), 'run: returns truncated key' );
|
||||
prv_assert( array_key_exists( 'run_id', $counts6 ), 'run: returns run_id key' );
|
||||
|
||||
// ── Test: run-lock refused when already locked ────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
setup_simple_run( [$sonar_row] );
|
||||
PRV_Run_Lock::acquire();
|
||||
$ledger7 = new PRV_Mock_Ledger();
|
||||
$runner7 = new PRV_Probe_Runner( $ledger7 );
|
||||
$counts7 = $runner7->run();
|
||||
prv_assert_equals( 0, $counts7['probed'], 'run-lock: 0 probed when locked' );
|
||||
prv_assert_equals( -1, $counts7['skipped_error'], 'run-lock: -1 sentinel when lock busy' );
|
||||
prv_assert_equals( 0, $ledger7->afford_count, 'run-lock: no can_afford checks when locked' );
|
||||
PRV_Run_Lock::release();
|
||||
|
||||
// ── Test: per-model health updated after run ──────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
|
||||
$runner8 = new PRV_Probe_Runner( new PRV_Mock_Ledger() );
|
||||
$runner8->run();
|
||||
|
||||
$models8 = PRV_Model_Registry::get_all();
|
||||
prv_assert( count( $models8 ) > 0, 'health update: at least 1 model' );
|
||||
// The sonar model should have health_status = healthy (probed=1, errors=0).
|
||||
foreach ( $models8 as $m ) {
|
||||
if ( 'perplexity/sonar' === $m['slug'] ) {
|
||||
prv_assert_equals( 'healthy', $m['health_status'], 'health update: sonar healthy after successful run' );
|
||||
prv_assert_equals( 1, $m['health_probed'], 'health update: health_probed = 1' );
|
||||
prv_assert_equals( 0, $m['health_errors'], 'health update: health_errors = 0' );
|
||||
}
|
||||
}
|
||||
|
||||
// ── Test: lock is released after a successful run ─────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
setup_simple_run( [$sonar_row] );
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = [
|
||||
['response' => ['code' => 200], 'body' => $cited_body],
|
||||
];
|
||||
$runner9 = new PRV_Probe_Runner( new PRV_Mock_Ledger() );
|
||||
$runner9->run();
|
||||
prv_assert( false === PRV_Run_Lock::is_locked(), 'run-lock cleanup: lock released after successful run' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
|
||||
121
tests/unit/test-projected-cost.php
Normal file
121
tests/unit/test-projected-cost.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for projected cost calculation and budget cap logic.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Projected Cost Tests ===\n";
|
||||
|
||||
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) {
|
||||
define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) {
|
||||
define( 'PRV_CF_ACCOUNT_ID', '' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) {
|
||||
define( 'PRV_CF_GATEWAY_ID', '' );
|
||||
}
|
||||
|
||||
// ── Test: projected cost reflects enabled models × peptides × intents ─────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
|
||||
// Set up a known config: 2 enabled models, 2 peptides, 2 intents.
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = [
|
||||
[ 'id' => 'mdl_a1', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
[ 'id' => 'mdl_a2', 'slug' => 'openai/gpt-4o-search-preview', 'provider' => 'openrouter', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ] = PRV_Model_Registry::SCHEMA_VERSION;
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ [ 'slug' => 'bpc-157', 'label' => 'BPC-157' ], [ 'slug' => 'tb-500', 'label' => 'TB-500' ] ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}', '{peptide} dosage' ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0;
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
|
||||
$cost = PRV_Config::get_projected_cost();
|
||||
|
||||
prv_assert_equals( 8, $cost['probe_count'], 'projected cost: 2 models × 2 peptides × 2 intents = 8 probes' );
|
||||
prv_assert( $cost['per_run_usd'] > 0.0, 'projected cost: per_run_usd > 0' );
|
||||
prv_assert( $cost['per_month_usd'] > 0.0, 'projected cost: per_month_usd > 0' );
|
||||
prv_assert( $cost['per_month_usd'] >= $cost['per_run_usd'], 'projected cost: monthly >= per-run' );
|
||||
prv_assert( false === $cost['over_cap'], 'projected cost: 8-probe config not over $5 cap' );
|
||||
|
||||
// ── Test: over_cap flag fires when projected monthly > cap ────────────────
|
||||
|
||||
prv_test_reset();
|
||||
// 1000 peptides × 3 intents × 3 models at $0.005/probe = $45/run (way over $5).
|
||||
$big_peptides = [];
|
||||
for ( $i = 0; $i < 100; $i++ ) {
|
||||
$big_peptides[] = [ 'slug' => "pep-{$i}", 'label' => "Peptide {$i}" ];
|
||||
}
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = [
|
||||
[ 'id' => 'mdl_b1', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
[ 'id' => 'mdl_b2', 'slug' => 'openai/gpt-4o-search-preview', 'provider' => 'openrouter', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
[ 'id' => 'mdl_b3', 'slug' => 'google/gemini-2.0-flash-001', 'provider' => 'openrouter', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ] = PRV_Model_Registry::SCHEMA_VERSION;
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = $big_peptides;
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}', '{peptide} dosage', '{peptide} guide' ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0;
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
|
||||
$cost_big = PRV_Config::get_projected_cost();
|
||||
prv_assert( $cost_big['over_cap'], 'projected cost: over_cap=true for large config over $5 cap' );
|
||||
|
||||
// ── Test: disabled models excluded from probe_count ───────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = [
|
||||
[ 'id' => 'mdl_c1', 'slug' => 'perplexity/sonar', 'provider' => 'perplexity', 'enabled' => true, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
[ 'id' => 'mdl_c2', 'slug' => 'openai/gpt-4o-search-preview', 'provider' => 'openrouter', 'enabled' => false, 'note' => '', 'health_status' => 'unknown', 'health_probed' => 0, 'health_errors' => 0, 'health_run_id' => null ],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Model_Registry::VERSION_KEY ] = PRV_Model_Registry::SCHEMA_VERSION;
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ [ 'slug' => 'bpc-157', 'label' => 'BPC-157' ] ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}' ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 5.0;
|
||||
$GLOBALS['prv_test_state']['options'][ PRV_Config::CADENCE_KEY ] = 'weekly';
|
||||
|
||||
$cost_disabled = PRV_Config::get_projected_cost();
|
||||
// 1 enabled model × 1 peptide × 1 intent = 1 probe.
|
||||
prv_assert_equals( 1, $cost_disabled['probe_count'], 'projected cost: disabled models excluded from probe_count' );
|
||||
|
||||
// ── Test: truncation flag set when budget hit mid-run ────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [
|
||||
[ 'slug' => 'bpc-157', 'label' => 'BPC-157' ],
|
||||
[ 'slug' => 'tb-500', 'label' => 'TB-500' ],
|
||||
[ 'slug' => 'mk-677', 'label' => 'MK-677' ],
|
||||
];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}' ];
|
||||
|
||||
class PRV_Exhausting_Ledger_Cost extends PRV_Cost_Ledger {
|
||||
private int $count = 0;
|
||||
public function can_afford( float $e ): bool { return $this->count++ < 1; }
|
||||
public function update_row_cost( int $id, float $c ): bool { return true; }
|
||||
public function get_month_to_date_usd(): float { return 4.99; }
|
||||
}
|
||||
|
||||
$cited_body = json_encode([
|
||||
'choices' => [['message' => ['content' => 'test']]],
|
||||
'citations' => ['https://peptiderepo.com/bpc-157/'],
|
||||
'usage' => ['total_tokens' => 50],
|
||||
]);
|
||||
// Queue enough responses.
|
||||
$GLOBALS['prv_test_state']['remote_posts'] = array_fill( 0, 3, ['response' => ['code' => 200], 'body' => $cited_body] );
|
||||
|
||||
$runner = new PRV_Probe_Runner( new PRV_Exhausting_Ledger_Cost() );
|
||||
$counts = $runner->run();
|
||||
|
||||
prv_assert( $counts['truncated'], 'truncation flag: set when budget hit mid-run' );
|
||||
prv_assert_equals( 1, (int) get_option( 'prv_last_run_truncated', 0 ), 'truncation flag: prv_last_run_truncated option set to 1' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
85
tests/unit/test-run-lock.php
Normal file
85
tests/unit/test-run-lock.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Run_Lock: acquire/release/is_locked concurrency guard.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Run Lock Tests ===\n";
|
||||
|
||||
// ── Test: acquire returns true when not locked ────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
prv_assert( PRV_Run_Lock::acquire(), 'acquire: returns true when lock is free' );
|
||||
|
||||
// ── Test: is_locked true while acquired ──────────────────────────────────
|
||||
|
||||
prv_assert( PRV_Run_Lock::is_locked(), 'is_locked: true immediately after acquire' );
|
||||
|
||||
// ── Test: second acquire returns false (lock busy) ───────────────────────
|
||||
|
||||
prv_assert( false === PRV_Run_Lock::acquire(), 'acquire: returns false when already locked' );
|
||||
|
||||
// ── Test: release clears the lock ────────────────────────────────────────
|
||||
|
||||
PRV_Run_Lock::release();
|
||||
prv_assert( false === PRV_Run_Lock::is_locked(), 'is_locked: false after release' );
|
||||
|
||||
// ── Test: acquire succeeds again after release ───────────────────────────
|
||||
|
||||
prv_assert( PRV_Run_Lock::acquire(), 'acquire: succeeds again after release' );
|
||||
PRV_Run_Lock::release();
|
||||
|
||||
// ── Test: locked_since returns null when not locked ──────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
prv_assert( null === PRV_Run_Lock::locked_since(), 'locked_since: null when not locked' );
|
||||
|
||||
// ── Test: locked_since returns non-null when locked ──────────────────────
|
||||
|
||||
PRV_Run_Lock::acquire();
|
||||
$since = PRV_Run_Lock::locked_since();
|
||||
prv_assert( null !== $since, 'locked_since: non-null when locked' );
|
||||
prv_assert( $since >= 0, 'locked_since: >= 0 seconds' );
|
||||
PRV_Run_Lock::release();
|
||||
|
||||
// ── Test: probe runner refuses when locked (skipped_error sentinel) ───────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
$GLOBALS['prv_test_state']['options']['prv_peptides'] = [ [ 'slug' => 'bpc-157', 'label' => 'BPC-157' ] ];
|
||||
$GLOBALS['prv_test_state']['options']['prv_prompt_intents'] = [ 'what is {peptide}' ];
|
||||
|
||||
// Pre-acquire the lock.
|
||||
PRV_Run_Lock::acquire();
|
||||
|
||||
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) {
|
||||
define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) {
|
||||
define( 'PRV_CF_ACCOUNT_ID', '' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) {
|
||||
define( 'PRV_CF_GATEWAY_ID', '' );
|
||||
}
|
||||
|
||||
$runner = new PRV_Probe_Runner( new PRV_Cost_Ledger() );
|
||||
$counts = $runner->run();
|
||||
|
||||
// When lock is busy, skipped_error is -1 (sentinel) and probed is 0.
|
||||
prv_assert_equals( 0, $counts['probed'], 'run-lock: runner returns 0 probed when locked' );
|
||||
prv_assert_equals( -1, $counts['skipped_error'], 'run-lock: skipped_error is -1 sentinel when lock busy' );
|
||||
|
||||
PRV_Run_Lock::release();
|
||||
|
||||
// ── Test: runner releases lock even on exception (lock cleanup) ──────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
prv_assert( false === PRV_Run_Lock::is_locked(), 'run-lock cleanup: lock released after runner completes' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
@@ -1,9 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for uninstall purge: table drop + options deletion.
|
||||
*
|
||||
* Exercises PRV_Table_Manager::drop_table() and verifies the uninstall
|
||||
* script logic (simulated because it requires WP_UNINSTALL_PLUGIN).
|
||||
* Tests for uninstall purge: all prv_ options deleted, lock cleared.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
@@ -12,90 +9,71 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== PRV Uninstall Purge Tests ===\n";
|
||||
echo "=== PRV Uninstall Tests ===\n";
|
||||
|
||||
// ── Mock $wpdb to track DROP calls ──────────────────────────────────────
|
||||
if ( ! defined( 'PRV_OPENROUTER_API_KEY' ) ) {
|
||||
define( 'PRV_OPENROUTER_API_KEY', 'sk-or-test-key' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_ACCOUNT_ID' ) ) {
|
||||
define( 'PRV_CF_ACCOUNT_ID', '' );
|
||||
}
|
||||
if ( ! defined( 'PRV_CF_GATEWAY_ID' ) ) {
|
||||
define( 'PRV_CF_GATEWAY_ID', '' );
|
||||
}
|
||||
|
||||
$GLOBALS['prv_test_state']['drop_calls'] = [];
|
||||
|
||||
// Extend the stub to record DROP statements.
|
||||
global $wpdb;
|
||||
$wpdb = new class extends stdClass_wpdb {
|
||||
public function query( string $sql ): bool {
|
||||
if ( str_contains( strtoupper( $sql ), 'DROP TABLE' ) ) {
|
||||
$GLOBALS['prv_test_state']['drop_calls'][] = $sql;
|
||||
}
|
||||
if ( str_contains( strtoupper( $sql ), 'DELETE FROM' ) && str_contains( $sql, "prv\\_" ) ) {
|
||||
$GLOBALS['prv_test_state']['options_deleted'] = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Test: drop_table issues DROP TABLE IF EXISTS ─────────────────────────
|
||||
// Populate all known prv_ options including v0.2.0 additions.
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['drop_calls'] = [];
|
||||
PRV_Table_Manager::drop_table();
|
||||
PRV_Model_Registry::run_migration_v2();
|
||||
PRV_Config_Version::maybe_seed_initial_version();
|
||||
update_option( 'prv_monthly_budget_usd', 5.0 );
|
||||
update_option( 'prv_peptides', [] );
|
||||
update_option( 'prv_prompt_intents', [] );
|
||||
update_option( PRV_Config::CADENCE_KEY, 'weekly' );
|
||||
update_option( 'prv_api_key_status', 'ok' );
|
||||
update_option( 'prv_api_key_last_check', '2026-06-14 12:00:00' );
|
||||
update_option( 'prv_last_run_at', '2026-06-14 12:00:00' );
|
||||
update_option( 'prv_last_run_counts', [] );
|
||||
update_option( 'prv_last_run_truncated', 0 );
|
||||
update_option( 'prv_last_run_truncated_at', '' );
|
||||
update_option( 'prv_schema_version', PRV_SCHEMA_VERSION );
|
||||
$GLOBALS['prv_test_state']['transients'][ PRV_Run_Lock::LOCK_KEY ] = time();
|
||||
$GLOBALS['prv_test_state']['cron_events'][ PRV_CRON_HOOK ] = [ 'timestamp' => time() + 3600, 'schedule' => 'weekly' ];
|
||||
|
||||
prv_assert( ! empty( $GLOBALS['prv_test_state']['drop_calls'] ), 'drop_table: DROP TABLE query executed' );
|
||||
$drop_sql = $GLOBALS['prv_test_state']['drop_calls'][0] ?? '';
|
||||
prv_assert( str_contains( strtoupper( $drop_sql ), 'DROP TABLE IF EXISTS' ), 'drop_table: uses DROP TABLE IF EXISTS' );
|
||||
prv_assert( str_contains( $drop_sql, 'prv_ai_visibility' ), 'drop_table: targets prv_ai_visibility table' );
|
||||
// Verify options are present before uninstall.
|
||||
prv_assert( false !== get_option( 'prv_models' ), 'pre-uninstall: prv_models option exists' );
|
||||
prv_assert( false !== get_option( 'prv_monthly_budget_usd' ), 'pre-uninstall: prv_monthly_budget_usd exists' );
|
||||
prv_assert( false !== get_option( 'prv_api_key_status' ), 'pre-uninstall: prv_api_key_status exists' );
|
||||
prv_assert( false !== get_option( 'prv_active_config_version' ), 'pre-uninstall: prv_active_config_version exists' );
|
||||
prv_assert( false !== get_transient( PRV_Run_Lock::LOCK_KEY ), 'pre-uninstall: run lock transient set' );
|
||||
prv_assert( PRV_Cron::is_scheduled(), 'pre-uninstall: cron scheduled' );
|
||||
|
||||
// ── Test: create_table sets the schema_version option ────────────────────
|
||||
// Simulate uninstall: delete all prv_ options.
|
||||
// In production, the SQL wildcard handles this; in test we iterate.
|
||||
$keys_to_purge = array_filter(
|
||||
array_keys( $GLOBALS['prv_test_state']['options'] ),
|
||||
static fn( $k ) => str_starts_with( $k, 'prv_' )
|
||||
);
|
||||
foreach ( $keys_to_purge as $k ) {
|
||||
unset( $GLOBALS['prv_test_state']['options'][ $k ] );
|
||||
}
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Table_Manager::create_table();
|
||||
$schema_ver = get_option( 'prv_schema_version' );
|
||||
prv_assert_equals( PRV_SCHEMA_VERSION, $schema_ver, 'create_table: prv_schema_version option set after create' );
|
||||
// Also delete transients.
|
||||
delete_transient( PRV_Run_Lock::LOCK_KEY );
|
||||
|
||||
// ── Test: get_table_name returns prefixed name ───────────────────────────
|
||||
// Clear cron.
|
||||
wp_clear_scheduled_hook( PRV_CRON_HOOK );
|
||||
|
||||
prv_test_reset();
|
||||
$table = PRV_Table_Manager::get_table_name();
|
||||
prv_assert_equals( 'wp_prv_ai_visibility', $table, 'get_table_name: returns wp_ prefixed table name' );
|
||||
|
||||
// ── Test: seed_defaults writes expected option keys ──────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Config::seed_defaults();
|
||||
|
||||
prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] ), 'seed_defaults: prv_monthly_budget_usd set' );
|
||||
prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_peptides'] ), 'seed_defaults: prv_peptides set' );
|
||||
prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_prompt_intents'] ), 'seed_defaults: prv_prompt_intents set' );
|
||||
prv_assert( isset( $GLOBALS['prv_test_state']['options']['prv_models'] ), 'seed_defaults: prv_models set' );
|
||||
|
||||
// ── Test: seed_defaults does not overwrite existing values ───────────────
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'] = 99.0;
|
||||
PRV_Config::seed_defaults();
|
||||
prv_assert_equals( 99.0, $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'], 'seed_defaults: does not overwrite existing prv_monthly_budget_usd' );
|
||||
|
||||
// ── Test: default budget is PRV_DEFAULT_MONTHLY_BUDGET_USD ──────────────
|
||||
|
||||
prv_test_reset();
|
||||
PRV_Config::seed_defaults();
|
||||
prv_assert_equals( PRV_DEFAULT_MONTHLY_BUDGET_USD, $GLOBALS['prv_test_state']['options']['prv_monthly_budget_usd'], 'seed_defaults: default budget = PRV_DEFAULT_MONTHLY_BUDGET_USD (5.0)' );
|
||||
|
||||
// ── Test: default peptide count is 12 ───────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$peptides = PRV_Config::get_peptides();
|
||||
prv_assert_equals( 12, count( $peptides ), 'get_peptides: default seed has 12 peptides' );
|
||||
|
||||
// ── Test: default prompt intent count is 3 ──────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$intents = PRV_Config::get_prompt_intents();
|
||||
prv_assert_equals( 3, count( $intents ), 'get_prompt_intents: default seed has 3 intents' );
|
||||
|
||||
// ── Test: default model count is 3 ──────────────────────────────────────
|
||||
|
||||
prv_test_reset();
|
||||
$models = PRV_Config::get_models();
|
||||
prv_assert_equals( 3, count( $models ), 'get_models: default seed has 3 models' );
|
||||
prv_assert( in_array( 'perplexity/sonar', $models, true ), 'get_models: perplexity/sonar in default models' );
|
||||
// Verify all prv_ options gone.
|
||||
prv_assert( false === get_option( 'prv_models' ), 'post-uninstall: prv_models deleted' );
|
||||
prv_assert( false === get_option( 'prv_monthly_budget_usd' ), 'post-uninstall: prv_monthly_budget_usd deleted' );
|
||||
prv_assert( false === get_option( 'prv_api_key_status' ), 'post-uninstall: prv_api_key_status deleted' );
|
||||
prv_assert( false === get_option( 'prv_active_config_version' ), 'post-uninstall: prv_active_config_version deleted' );
|
||||
prv_assert( false === get_option( 'prv_config_versions' ), 'post-uninstall: prv_config_versions deleted' );
|
||||
prv_assert( false === get_option( 'prv_models_schema_version' ), 'post-uninstall: prv_models_schema_version deleted' );
|
||||
prv_assert( false === get_option( 'prv_schema_version' ), 'post-uninstall: prv_schema_version deleted' );
|
||||
prv_assert( false === get_option( 'prv_last_run_truncated' ), 'post-uninstall: prv_last_run_truncated deleted' );
|
||||
prv_assert( false === get_transient( PRV_Run_Lock::LOCK_KEY ), 'post-uninstall: run lock transient cleared' );
|
||||
prv_assert( false === PRV_Cron::is_scheduled(), 'post-uninstall: cron cleared' );
|
||||
|
||||
exit( prv_test_summary() );
|
||||
|
||||
Reference in New Issue
Block a user