v0.2.1 — dashboard visuals + deploy pipeline
Piece 1 — Visuals - Add KPI bento bar (score tile with run-health pill, cost tile, last-run tile). Run-health pill shows 'All models healthy' / 'N model(s) degraded' / 'No run yet' sourced from PRV_Model_Registry health fields via collector. - Add config-change trendline marker: inline configMarker Chart.js plugin draws a vertical dashed orange line + pin at every config_version boundary. Legend swatch + annotation note appear when breaks exist. - Chart sits in fixed .prv-chartbox (300px); typeof-Chart guard + .prv-noscript fallback — cannot grow layout, degrades gracefully offline. - PRV_Dashboard_Renderer: new class owns bento tile + trendline rendering. PRV_Ai_Visibility_Panel is now a thin orchestrator (237 lines). - PRV_Ai_Visibility_Collector: trendline query now includes MIN(config_version) per run; collect() adds last_run_counts and config_versions to payload. - PRV_Admin_Page: get_dashboard_css() returns Assay dark palette CSS. Piece 2 — Deploy pipeline - .github/workflows/deploy.yml: on push to main, runs CI, rsyncs plugin to Hostinger PROD (excl. .git .github tests phpunit composer vendor phpcs *.md), purges LiteSpeed, health-checks (200/403). Standalone — not smoke-gated. DoD - Version 0.2.1 in header + constant; CHANGELOG, ARCHITECTURE, CONVENTIONS, CONTEXT all updated same commit. - tests/unit/test-dashboard-panel.php: 27 assertions. - Pre-push checks: no PHP built-in name shadows in bootstrap.php; all includes/*.php files <= 300 lines (max: 298).
This commit is contained in:
264
tests/unit/test-dashboard-panel.php
Normal file
264
tests/unit/test-dashboard-panel.php
Normal file
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
/**
|
||||
* Tests for PRV_Ai_Visibility_Panel and PRV_Dashboard_Renderer data-shaping added
|
||||
* in v0.2.1: config-version trendline annotation, KPI health pill, and collector
|
||||
* last_run_counts / config_versions payload fields.
|
||||
*
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
|
||||
echo "=== test-dashboard-panel.php ===\n";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Invoke a private or protected method on an object instance.
|
||||
*
|
||||
* @param object $obj Instance to invoke on.
|
||||
* @param string $method Method name.
|
||||
* @param array $args Arguments.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function prv_call_private( object $obj, string $method, array $args = array() ) {
|
||||
$ref = new ReflectionMethod( $obj, $method );
|
||||
$ref->setAccessible( true );
|
||||
return $ref->invokeArgs( $obj, $args );
|
||||
}
|
||||
|
||||
// ── 1. Panel: derive_health_pill_state ───────────────────────────────────────
|
||||
|
||||
echo "\n-- derive_health_pill_state --\n";
|
||||
|
||||
$panel = new PRV_Ai_Visibility_Panel();
|
||||
|
||||
$result = prv_call_private( $panel, 'derive_health_pill_state', array( array() ) );
|
||||
prv_assert_equals( 'neutral', $result['state'], 'health pill: empty map → neutral' );
|
||||
|
||||
$result = prv_call_private(
|
||||
$panel,
|
||||
'derive_health_pill_state',
|
||||
array(
|
||||
array(
|
||||
'slug-a' => array( 'health_status' => 'healthy' ),
|
||||
'slug-b' => array( 'health_status' => 'healthy' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
prv_assert_equals( 'ok', $result['state'], 'health pill: all healthy → ok' );
|
||||
prv_assert( false !== strpos( $result['label'], 'healthy' ), 'health pill: ok label contains "healthy"' );
|
||||
|
||||
$result = prv_call_private(
|
||||
$panel,
|
||||
'derive_health_pill_state',
|
||||
array(
|
||||
array(
|
||||
'slug-a' => array( 'health_status' => 'healthy' ),
|
||||
'slug-b' => array( 'health_status' => 'retired' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
prv_assert_equals( 'warn', $result['state'], 'health pill: one retired → warn' );
|
||||
prv_assert( false !== strpos( $result['label'], '1' ), 'health pill: warn label contains count "1"' );
|
||||
|
||||
$result = prv_call_private(
|
||||
$panel,
|
||||
'derive_health_pill_state',
|
||||
array(
|
||||
array(
|
||||
'slug-a' => array( 'health_status' => 'retired' ),
|
||||
'slug-b' => array( 'health_status' => 'retired' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
prv_assert_equals( 'warn', $result['state'], 'health pill: two retired → warn' );
|
||||
prv_assert( false !== strpos( $result['label'], '2' ), 'health pill: warn label contains count "2"' );
|
||||
|
||||
$result = prv_call_private(
|
||||
$panel,
|
||||
'derive_health_pill_state',
|
||||
array( array( 'slug-a' => array( 'health_status' => 'disabled' ) ) )
|
||||
);
|
||||
prv_assert_equals( 'neutral', $result['state'], 'health pill: disabled-only → neutral' );
|
||||
|
||||
$result = prv_call_private(
|
||||
$panel,
|
||||
'derive_health_pill_state',
|
||||
array( array( 'slug-a' => array( 'health_status' => 'unknown' ) ) )
|
||||
);
|
||||
prv_assert_equals( 'neutral', $result['state'], 'health pill: unknown status → neutral' );
|
||||
|
||||
// ── 2. Dashboard Renderer: build_delta_html ──────────────────────────────────
|
||||
|
||||
echo "\n-- PRV_Dashboard_Renderer::build_delta_html --\n";
|
||||
|
||||
$renderer = new PRV_Dashboard_Renderer();
|
||||
|
||||
$delta_up = $renderer->build_delta_html( 0.10, 0.08 );
|
||||
$delta_down = $renderer->build_delta_html( 0.05, 0.08 );
|
||||
$delta_flat = $renderer->build_delta_html( 0.08, 0.08 );
|
||||
$delta_null = $renderer->build_delta_html( 0.08, null );
|
||||
|
||||
prv_assert( false !== strpos( $delta_up, 'prv-delta--up' ), 'delta: positive → --up class' );
|
||||
prv_assert( false !== strpos( $delta_up, '+' ), 'delta: positive → "+" prefix' );
|
||||
prv_assert( false !== strpos( $delta_down, 'prv-delta--down' ), 'delta: negative → --down class' );
|
||||
prv_assert( false !== strpos( $delta_flat, 'prv-delta--flat' ), 'delta: zero → --flat class' );
|
||||
prv_assert_equals( '', $delta_null, 'delta: null previous → empty string' );
|
||||
|
||||
// Precision: 0.10 - 0.08 = +2.00 pp
|
||||
prv_assert( false !== strpos( $delta_up, '2.00' ), 'delta: value formatted to 2dp' );
|
||||
|
||||
// ── 3. Collector: trendline includes config_version ──────────────────────────
|
||||
|
||||
echo "\n-- collector trendline config_version field --\n";
|
||||
|
||||
prv_test_reset();
|
||||
|
||||
$GLOBALS['prv_test_state']['wpdb_results'] = array(
|
||||
array(
|
||||
'run_id' => 'run-001',
|
||||
'captured_at' => '2026-06-01 10:00:00',
|
||||
'cited_count' => 2,
|
||||
'total_count' => 10,
|
||||
'position_sum' => 0.5,
|
||||
'config_version' => 1,
|
||||
),
|
||||
array(
|
||||
'run_id' => 'run-002',
|
||||
'captured_at' => '2026-06-08 10:00:00',
|
||||
'cited_count' => 3,
|
||||
'total_count' => 10,
|
||||
'position_sum' => 0.6,
|
||||
'config_version' => 2,
|
||||
),
|
||||
);
|
||||
$GLOBALS['prv_test_state']['wpdb_var'] = '2026-06-08 10:00:00';
|
||||
|
||||
$collector = new PRV_Ai_Visibility_Collector();
|
||||
$data = $collector->collect();
|
||||
|
||||
prv_assert( isset( $data['trendline'] ), 'collector: trendline key present' );
|
||||
prv_assert_equals( 2, count( $data['trendline'] ), 'collector: two trendline rows' );
|
||||
prv_assert( array_key_exists( 'config_version', $data['trendline'][0] ), 'trendline[0] has config_version key' );
|
||||
prv_assert_equals( 1, $data['trendline'][0]['config_version'], 'trendline[0].config_version = 1' );
|
||||
prv_assert_equals( 2, $data['trendline'][1]['config_version'], 'trendline[1].config_version = 2' );
|
||||
|
||||
// ── 4. Collector: last_run_counts and config_versions keys present ────────────
|
||||
|
||||
echo "\n-- collector: new payload keys --\n";
|
||||
|
||||
prv_assert( isset( $data['last_run_counts'] ), 'collector: last_run_counts key present' );
|
||||
prv_assert( is_array( $data['last_run_counts'] ), 'collector: last_run_counts is array' );
|
||||
prv_assert( isset( $data['config_versions'] ), 'collector: config_versions key present' );
|
||||
prv_assert( is_array( $data['config_versions'] ), 'collector: config_versions is array' );
|
||||
|
||||
// ── 5. Collector: null config_version handled gracefully ─────────────────────
|
||||
|
||||
echo "\n-- collector: null config_version row --\n";
|
||||
|
||||
prv_test_reset();
|
||||
$GLOBALS['prv_test_state']['wpdb_results'] = array(
|
||||
array(
|
||||
'run_id' => 'run-001',
|
||||
'captured_at' => '2026-06-01 10:00:00',
|
||||
'cited_count' => 1,
|
||||
'total_count' => 5,
|
||||
'position_sum' => 0.25,
|
||||
'config_version' => null,
|
||||
),
|
||||
);
|
||||
$GLOBALS['prv_test_state']['wpdb_var'] = '2026-06-01 10:00:00';
|
||||
|
||||
$collector2 = new PRV_Ai_Visibility_Collector();
|
||||
$data2 = $collector2->collect();
|
||||
|
||||
prv_assert_equals( 1, count( $data2['trendline'] ), 'null config_version row included' );
|
||||
prv_assert( null === $data2['trendline'][0]['config_version'], 'null config_version preserved as null' );
|
||||
|
||||
// ── 6. Collector: collect_model_health from registry ─────────────────────────
|
||||
|
||||
echo "\n-- collector: collect_model_health --\n";
|
||||
|
||||
prv_test_reset();
|
||||
|
||||
$GLOBALS['prv_test_state']['options']['prv_models_schema_version'] = 2;
|
||||
$GLOBALS['prv_test_state']['options']['prv_models'] = array(
|
||||
array(
|
||||
'id' => 'mdl_aaa',
|
||||
'slug' => 'perplexity/sonar',
|
||||
'provider' => 'perplexity',
|
||||
'enabled' => true,
|
||||
'note' => '',
|
||||
'health_status' => 'healthy',
|
||||
'health_probed' => 10,
|
||||
'health_errors' => 0,
|
||||
'health_run_id' => 'run-001',
|
||||
),
|
||||
array(
|
||||
'id' => 'mdl_bbb',
|
||||
'slug' => 'openai/gpt-4o-search-preview',
|
||||
'provider' => 'openrouter',
|
||||
'enabled' => true,
|
||||
'note' => '',
|
||||
'health_status' => 'retired',
|
||||
'health_probed' => 0,
|
||||
'health_errors' => 5,
|
||||
'health_run_id' => 'run-001',
|
||||
),
|
||||
);
|
||||
$GLOBALS['prv_test_state']['wpdb_results'] = array();
|
||||
$GLOBALS['prv_test_state']['wpdb_var'] = null;
|
||||
|
||||
$collector3 = new PRV_Ai_Visibility_Collector();
|
||||
$data3 = $collector3->collect();
|
||||
$last_run_counts = $data3['last_run_counts'];
|
||||
|
||||
prv_assert( isset( $last_run_counts['perplexity/sonar'] ), 'sonar slug present in last_run_counts' );
|
||||
prv_assert_equals( 'healthy', $last_run_counts['perplexity/sonar']['health_status'], 'sonar = healthy' );
|
||||
prv_assert( isset( $last_run_counts['openai/gpt-4o-search-preview'] ), 'gpt slug present in last_run_counts' );
|
||||
prv_assert_equals( 'retired', $last_run_counts['openai/gpt-4o-search-preview']['health_status'], 'gpt = retired' );
|
||||
|
||||
// Health pill should be warn from this data.
|
||||
$pill = prv_call_private( $panel, 'derive_health_pill_state', array( $last_run_counts ) );
|
||||
prv_assert_equals( 'warn', $pill['state'], 'health pill from registry data → warn' );
|
||||
|
||||
// ── 7. Renderer: trendline marker detection ───────────────────────────────────
|
||||
|
||||
echo "\n-- renderer: config-change marker detection (output capture) --\n";
|
||||
|
||||
// Verify that a single-version trendline produces no cfg-note.
|
||||
ob_start();
|
||||
$renderer->render_trendline(
|
||||
array(
|
||||
array( 'run_id' => 'r1', 'captured_at' => '2026-06-01 10:00:00', 'score' => 0.0, 'config_version' => 1 ),
|
||||
array( 'run_id' => 'r2', 'captured_at' => '2026-06-08 10:00:00', 'score' => 0.0, 'config_version' => 1 ),
|
||||
)
|
||||
);
|
||||
$out_no_marker = ob_get_clean();
|
||||
prv_assert( false === strpos( $out_no_marker, 'prv-cfg-note' ), 'no marker: no cfg-note rendered when config version unchanged' );
|
||||
prv_assert( false !== strpos( $out_no_marker, 'prv-trendline-chart' ), 'no marker: canvas present' );
|
||||
|
||||
// Verify that a two-version trendline produces cfg-note and markerIdx.
|
||||
ob_start();
|
||||
$renderer->render_trendline(
|
||||
array(
|
||||
array( 'run_id' => 'r1', 'captured_at' => '2026-06-01 10:00:00', 'score' => 0.0, 'config_version' => 1 ),
|
||||
array( 'run_id' => 'r2', 'captured_at' => '2026-06-08 10:00:00', 'score' => 0.0, 'config_version' => 2 ),
|
||||
)
|
||||
);
|
||||
$out_with_marker = ob_get_clean();
|
||||
prv_assert( false !== strpos( $out_with_marker, 'prv-cfg-note' ), 'marker: cfg-note rendered when config version changes' );
|
||||
prv_assert( false !== strpos( $out_with_marker, 'configMarker' ), 'marker: configMarker plugin JS emitted' );
|
||||
prv_assert( false !== strpos( $out_with_marker, 'prv-leg-cfg' ), 'marker: legend swatch present' );
|
||||
|
||||
// Verify graceful-degradation path is present.
|
||||
prv_assert( false !== strpos( $out_with_marker, 'prv-noscript' ), 'trendline: noscript fallback class in JS guard' );
|
||||
prv_assert( false !== strpos( $out_with_marker, 'prv-chart-fallback' ), 'trendline: fallback note element present' );
|
||||
|
||||
// ── Summary ──────────────────────────────────────────────────────────────────
|
||||
|
||||
exit( prv_test_summary() );
|
||||
Reference in New Issue
Block a user