Fix CI round 2: InterpolatedNotPrepared + executor WPCS + test stub
class-prv-cost-rollup-query.php: - get_mtd_summary + get_model_breakdown: phpcs:disable/enable block for InterpolatedNotPrepared (single-line ignore doesn't cover multi-line string spans) class-prv-probe-run-executor.php: - Reduced to 290 lines (well under 300) - write_meta array calls: phpcs:disable ArrayItemNoNewLine for compact 2-per-line format; write_io try/catch explicit comment on empty catch - persist_result: expanded insert array to one item per line - Class docblock consolidated; property @var docblocks to one-line format - Closing class brace: removed extra blank line (PSR2 CloseBraceAfterBody) tests/bootstrap.php + test-cost-audit.php: - Added get_row() stub to stdClass_wpdb (PRV_Cost_Rollup_Query uses get_row for get_mtd_summary, not get_var) - Added wpdb_row key to initial state + prv_test_reset() - Test 1: seed wpdb_row with expected row data instead of wpdb_var
This commit is contained in:
@@ -42,10 +42,9 @@ class PRV_Cost_Rollup_Query {
|
||||
$table = PRV_Call_Meta_Table::get_table_name();
|
||||
$month = gmdate( 'Y-m' );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$row = $wpdb->get_row(
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT COALESCE(SUM(cost_usd),0) AS total_cost, COUNT(*) AS total_calls
|
||||
FROM {$table}
|
||||
WHERE DATE_FORMAT(captured_at, %s) = %s",
|
||||
@@ -54,6 +53,7 @@ class PRV_Cost_Rollup_Query {
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
$total_cost = (float) ( $row['total_cost'] ?? 0.0 );
|
||||
$total_calls = (int) ( $row['total_calls'] ?? 0 );
|
||||
@@ -79,10 +79,9 @@ class PRV_Cost_Rollup_Query {
|
||||
$table = PRV_Call_Meta_Table::get_table_name();
|
||||
$month = gmdate( 'Y-m' );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT model,
|
||||
COUNT(*) AS calls,
|
||||
COALESCE(SUM(cost_usd),0) AS mtd_cost
|
||||
@@ -96,6 +95,7 @@ class PRV_Cost_Rollup_Query {
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
// phpcs:enable
|
||||
|
||||
$result = array();
|
||||
foreach ( (array) $rows as $row ) {
|
||||
|
||||
@@ -1,36 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Executes the inner loop of a probe run (v0.3.0 extraction).
|
||||
* Inner probe loop executor split from PRV_Probe_Runner (v0.3.0).
|
||||
* P0: can_afford → probe → persist → update_cost → write_meta → [swallowed] capture_io.
|
||||
*
|
||||
* @see class-prv-probe-runner.php — Acquires run-lock + calls execute().
|
||||
* @see class-prv-capture-writer.php — Allowlist-only capture writer.
|
||||
* @package PrVision
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Executes one probe run's inner loop: peptides × intents × models.
|
||||
* Split from PRV_Probe_Runner to stay under 300 lines.
|
||||
*
|
||||
* P0 order: can_afford → probe → persist_result → update_row_cost → write_meta → [swallowed] capture_io.
|
||||
*
|
||||
* @see class-prv-probe-runner.php — Acquires lock + calls execute().
|
||||
* @see class-prv-capture-writer.php — Allowlist-only capture writer (P0).
|
||||
* @package PrVision
|
||||
*/
|
||||
class PRV_Probe_Run_Executor {
|
||||
|
||||
/**
|
||||
* Budget ledger for cost-cap enforcement.
|
||||
*
|
||||
* @var PRV_Cost_Ledger
|
||||
*/
|
||||
/** @var PRV_Cost_Ledger Budget ledger for cost-cap enforcement. */
|
||||
private PRV_Cost_Ledger $ledger;
|
||||
|
||||
/**
|
||||
* Per-call capture writer (best-effort; never in critical path).
|
||||
*
|
||||
* @var PRV_Capture_Writer
|
||||
*/
|
||||
/** @var PRV_Capture_Writer Per-call capture writer (best-effort; never in critical path). */
|
||||
private PRV_Capture_Writer $capture;
|
||||
|
||||
/**
|
||||
@@ -47,10 +31,8 @@ class PRV_Probe_Run_Executor {
|
||||
/**
|
||||
* Run the full peptide × intent × model loop.
|
||||
*
|
||||
* Side effects: HTTP calls to LLM APIs, database writes, option writes.
|
||||
*
|
||||
* @param string $run_id UUID for this run.
|
||||
* @param int $config_ver Config version at run time.
|
||||
* @param int $config_ver Config version.
|
||||
* @return array{probed: int, skipped_budget: int, skipped_error: int, truncated: bool, run_id: string}
|
||||
*/
|
||||
public function execute(
|
||||
@@ -105,18 +87,18 @@ class PRV_Probe_Run_Executor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute one probe combination in mandatory P0 order.
|
||||
* Execute one probe combination: can_afford → probe → persist → capture (P0 order).
|
||||
*
|
||||
* @param string $run_id Run UUID.
|
||||
* @param array<string,string> $peptide Peptide config.
|
||||
* @param string $model Model slug.
|
||||
* @param string $intent_tpl Intent template.
|
||||
* @param string $query Rendered query.
|
||||
* @param int $config_ver Config version.
|
||||
* @param array<string,mixed> &$counts Counts (modified).
|
||||
* @param array<string,array{probed:int,errors:int}> &$model_outcomes Outcomes (modified).
|
||||
* @param bool &$budget_hit Budget flag (modified).
|
||||
* @return void Modifies $counts and $model_outcomes in-place.
|
||||
* @param string $run_id Run UUID.
|
||||
* @param array<string,string> $peptide Peptide config.
|
||||
* @param string $model Model slug.
|
||||
* @param string $intent_tpl Intent template.
|
||||
* @param string $query Rendered query string.
|
||||
* @param int $config_ver Config version.
|
||||
* @param array<string,mixed> &$counts Counts accumulator.
|
||||
* @param array<string,array> &$model_outcomes Per-model outcome map.
|
||||
* @param bool &$budget_hit Budget exhaustion flag.
|
||||
* @return void
|
||||
*/
|
||||
private function probe_one(
|
||||
string $run_id,
|
||||
@@ -161,22 +143,23 @@ class PRV_Probe_Run_Executor {
|
||||
if ( isset( $model_outcomes[ $model ] ) ) {
|
||||
++$model_outcomes[ $model ]['errors'];
|
||||
}
|
||||
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
|
||||
$call_id = $this->capture->write_meta( array(
|
||||
'visibility_row' => null, 'run_id' => $run_id,
|
||||
'peptide_slug' => $peptide['slug'], 'model' => $model,
|
||||
'intent_label' => $intent_tpl, 'tokens_in' => null,
|
||||
'tokens_out' => null, 'cost_usd' => 0.0,
|
||||
'latency_ms' => $latency_ms, 'cited' => null,
|
||||
'http_status' => $http_status, 'config_version' => $config_ver,
|
||||
) );
|
||||
// phpcs:enable
|
||||
if ( $call_id > 0 ) {
|
||||
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
|
||||
$cid = $this->capture->write_meta(
|
||||
array(
|
||||
'visibility_row' => null, 'run_id' => $run_id,
|
||||
'peptide_slug' => $peptide['slug'], 'model' => $model,
|
||||
'intent_label' => $intent_tpl, 'tokens_in' => null,
|
||||
'tokens_out' => null, 'cost_usd' => 0.0,
|
||||
'latency_ms' => $latency_ms, 'cited' => null,
|
||||
'http_status' => $http_status, 'config_version' => $config_ver,
|
||||
)
|
||||
); // phpcs:enable
|
||||
if ( $cid > 0 ) {
|
||||
try {
|
||||
$this->capture->write_io( $call_id, $query, '' );
|
||||
} catch ( \Throwable $io_err ) {
|
||||
$this->capture->write_io( $cid, $query, '' );
|
||||
} catch ( \Throwable $e2 ) {
|
||||
// Best-effort: I/O capture failure must not propagate (P0-4).
|
||||
unset( $io_err );
|
||||
unset( $e2 );
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -187,22 +170,23 @@ class PRV_Probe_Run_Executor {
|
||||
$this->ledger->update_row_cost( $row_id, $result->get_cost_usd() );
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
|
||||
$call_id = $this->capture->write_meta( array(
|
||||
'visibility_row' => $row_id > 0 ? $row_id : null, 'run_id' => $run_id,
|
||||
'peptide_slug' => $peptide['slug'], 'model' => $model,
|
||||
'intent_label' => $intent_tpl, 'tokens_in' => $result->get_tokens_in(),
|
||||
'tokens_out' => $result->get_tokens_out(), 'cost_usd' => $result->get_cost_usd(),
|
||||
'latency_ms' => $latency_ms, 'cited' => $result->is_cited() ? 1 : 0,
|
||||
'http_status' => $http_status, 'config_version' => $config_ver,
|
||||
) );
|
||||
// phpcs:enable
|
||||
if ( $call_id > 0 ) {
|
||||
// phpcs:disable WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
|
||||
$cid = $this->capture->write_meta(
|
||||
array(
|
||||
'visibility_row' => $row_id > 0 ? $row_id : null, 'run_id' => $run_id,
|
||||
'peptide_slug' => $peptide['slug'], 'model' => $model,
|
||||
'intent_label' => $intent_tpl, 'tokens_in' => $result->get_tokens_in(),
|
||||
'tokens_out' => $result->get_tokens_out(), 'cost_usd' => $result->get_cost_usd(),
|
||||
'latency_ms' => $latency_ms, 'cited' => $result->is_cited() ? 1 : 0,
|
||||
'http_status' => $http_status, 'config_version' => $config_ver,
|
||||
)
|
||||
); // phpcs:enable
|
||||
if ( $cid > 0 ) {
|
||||
try {
|
||||
$this->capture->write_io( $call_id, $query, $result->get_raw_excerpt() );
|
||||
} catch ( \Throwable $io_err ) {
|
||||
$this->capture->write_io( $cid, $query, $result->get_raw_excerpt() );
|
||||
} catch ( \Throwable $e2 ) {
|
||||
// Best-effort: I/O capture failure must not propagate (P0-4).
|
||||
unset( $io_err );
|
||||
unset( $e2 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,18 +210,25 @@ class PRV_Probe_Run_Executor {
|
||||
private function persist_result( string $run_id, array $peptide, string $model, string $intent_tpl, PRV_Probe_Result $result, int $config_ver ): int {
|
||||
global $wpdb;
|
||||
$table = PRV_Table_Manager::get_table_name();
|
||||
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.Arrays.ArrayDeclarationSpacing.ArrayItemNoNewLine
|
||||
$wpdb->insert( $table, array(
|
||||
'run_id' => $run_id, 'captured_at' => current_time( 'mysql', true ),
|
||||
'peptide_slug' => $peptide['slug'], 'peptide_label' => $peptide['label'],
|
||||
'model' => $model, 'prompt_intent' => $intent_tpl,
|
||||
'cited' => $result->is_cited() ? 1 : 0, 'our_position' => $result->get_our_position(),
|
||||
'source_domains' => wp_json_encode( $result->get_source_domains() ),
|
||||
'raw_excerpt' => $result->get_raw_excerpt(),
|
||||
'cost_usd' => number_format( $result->get_cost_usd(), 8, '.', '' ),
|
||||
'config_version' => $config_ver,
|
||||
), array( '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%d' ) );
|
||||
// phpcs:enable
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'run_id' => $run_id,
|
||||
'captured_at' => current_time( 'mysql', true ),
|
||||
'peptide_slug' => $peptide['slug'],
|
||||
'peptide_label' => $peptide['label'],
|
||||
'model' => $model,
|
||||
'prompt_intent' => $intent_tpl,
|
||||
'cited' => $result->is_cited() ? 1 : 0,
|
||||
'our_position' => $result->get_our_position(),
|
||||
'source_domains' => wp_json_encode( $result->get_source_domains() ),
|
||||
'raw_excerpt' => $result->get_raw_excerpt(),
|
||||
'cost_usd' => number_format( $result->get_cost_usd(), 8, '.', '' ),
|
||||
'config_version' => $config_ver,
|
||||
),
|
||||
array( '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s', '%s', '%s', '%d' )
|
||||
);
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ $GLOBALS['prv_test_state'] = [
|
||||
'wpdb_insert_id' => 1,
|
||||
'wpdb_results' => [],
|
||||
'wpdb_var' => null,
|
||||
'wpdb_row' => null,
|
||||
'cron_events' => [],
|
||||
'remote_posts' => [],
|
||||
'wpdb_call_meta_rows' => [],
|
||||
@@ -65,6 +66,7 @@ function prv_test_reset(): void {
|
||||
'wpdb_insert_id' => 1,
|
||||
'wpdb_results' => [],
|
||||
'wpdb_var' => null,
|
||||
'wpdb_row' => null,
|
||||
'cron_events' => [],
|
||||
'remote_posts' => [],
|
||||
'wpdb_call_meta_rows' => [],
|
||||
@@ -260,6 +262,7 @@ class stdClass_wpdb {
|
||||
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_row( string $query, string $output = 'OBJECT' ): mixed { return $GLOBALS['prv_test_state']['wpdb_row'] ?? null; }
|
||||
public function get_var( string $query ): mixed { return $GLOBALS['prv_test_state']['wpdb_var']; }
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,16 @@ echo "=== test-cost-audit ===\n";
|
||||
prv_test_reset();
|
||||
|
||||
// ── Test 1: per-call cost sum reconciles to MTD via PRV_Cost_Rollup_Query ─
|
||||
// Seed the wpdb_var mock (returned by get_var) to simulate MTD total.
|
||||
// Seed get_row mock (used by get_mtd_summary) and get_var (used by PRV_Cost_Ledger).
|
||||
$GLOBALS['prv_test_state']['wpdb_row'] = array( 'total_cost' => '1.23456789', 'total_calls' => '5' );
|
||||
$GLOBALS['prv_test_state']['wpdb_var'] = '1.23456789';
|
||||
$rollup = new PRV_Cost_Rollup_Query();
|
||||
$rollup = new PRV_Cost_Rollup_Query();
|
||||
$summary = $rollup->get_mtd_summary();
|
||||
prv_assert( $summary['total_cost'] > 0, 'get_mtd_summary returns positive cost when rows present' );
|
||||
|
||||
// Reconcile: the same wpdb_var stub also underlies PRV_Cost_Ledger.
|
||||
$ledger = new PRV_Cost_Ledger();
|
||||
// Reconcile: PRV_Cost_Ledger reads get_var; confirm it agrees with the seeded mock.
|
||||
$ledger = new PRV_Cost_Ledger();
|
||||
$mtd_via_ledger = $ledger->get_month_to_date_usd();
|
||||
// Both read the same mock; confirm they agree.
|
||||
prv_assert_equals( (float) '1.23456789', $mtd_via_ledger, 'PRV_Cost_Ledger MTD matches mock value' );
|
||||
|
||||
// ── Test 2: project_month_end returns a numeric value ────────────────────
|
||||
|
||||
Reference in New Issue
Block a user