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;
|
||||
|
||||
Reference in New Issue
Block a user