fix: QA P1 findings on auto-schema-upgrade (v4.4.3)

Addresses the three P1 findings from QA baseline review of a8a0ce2:

1. uninstall.php: delete psa_db_version option on plugin removal so no
   orphaned data is left behind — satisfies AGENT-OPERATING-STANDARD
   teardown/uninstall DoD.

2. Extract psa_upgrade_needed( $stored, $current ) as a pure function and
   add tests/unit/UpgradeTest.php covering fresh-install, same-version,
   future-version, and older-version scenarios. The impure wrapper
   psa_maybe_run_upgrades() now delegates the decision to the pure helper.

3. Document the psa_db_version option and the admin_init-triggered
   upgrade mechanism in ARCHITECTURE.md (Options section), including
   why admin_init, downgrade behavior, cleanup, and testability.

No behavioral change for end users — same upgrade semantics, just
testable and documented, with proper teardown.

Agent-Session: cowork-qa-p1-fixes-20260414
This commit is contained in:
peptiderepo
2026-04-14 19:09:49 +07:00
parent 3820bdfd28
commit 88a4ae9a4e
4 changed files with 119 additions and 1 deletions

View File

@@ -356,6 +356,22 @@ $psa_settings = array(
);
```
### Schema versioning & automatic upgrades
The plugin stores the schema version it last migrated to in a separate option, `psa_db_version`. This is distinct from `PSA_VERSION` (the running code's semver), and lets the plugin detect whether the database needs to catch up after an in-place file update.
- **Option name:** `psa_db_version` (string, semver).
- **Default:** `'0.0.0'` when the option has never been written (fresh install observed from the admin side before activation-time migration).
- **Writer:** `psa_maybe_run_upgrades()` in `peptide-search-ai.php`, called on every `admin_init`. The function:
1. Reads `psa_db_version`.
2. Calls the pure helper `psa_upgrade_needed( $stored, PSA_VERSION )`, which is a thin wrapper over `version_compare` (stored < current → upgrade).
3. If an upgrade is needed, re-runs `PSA_Cost_Tracker::create_table()` so `dbDelta` picks up any additive schema changes (new columns, new indexes).
4. Writes `update_option( 'psa_db_version', PSA_VERSION, false )` so the upgrade doesn't re-run on the next request.
- **Why on `admin_init`:** dbDelta is safe-to-repeat but not free. Running it once when an admin visits after an update — rather than on every frontend page load — keeps the cost near zero without requiring users to deactivate/reactivate the plugin.
- **Downgrade / rollback:** if the running code is older than `psa_db_version` (e.g., deploy rolled back), the function is a no-op. The newer schema is left intact; old code continues to work against it as long as changes are additive.
- **Cleanup:** `uninstall.php` deletes `psa_db_version` alongside the other plugin options.
- **Testability:** `psa_upgrade_needed()` is a pure function (no WordPress calls) and is covered by `tests/unit/UpgradeTest.php` across fresh-install, same-version, future-version, and older-version scenarios.
## Frontend Architecture
### JavaScript (jQuery-based)

View File

@@ -52,6 +52,19 @@ function psa_admin_init() {
}
add_action( 'admin_init', 'psa_admin_init' );
/**
* Decide whether a schema upgrade is needed given a stored version and the
* currently-installed code version. Pure function — no side effects, testable
* in isolation.
*
* @param string $stored_version Version string recorded in psa_db_version.
* @param string $current_version Version string of the running code.
* @return bool True if an upgrade must run; false otherwise.
*/
function psa_upgrade_needed( $stored_version, $current_version ) {
return version_compare( $stored_version, $current_version, '<' );
}
/**
* Run database upgrades when the installed code version is newer than the
* stored `psa_db_version` option. Keeps users from needing to
@@ -60,11 +73,14 @@ add_action( 'admin_init', 'psa_admin_init' );
* Triggered on admin_init so dbDelta runs once per upgrade, not on every
* frontend page load.
*
* Side effects: reads psa_db_version option; on upgrade, calls
* PSA_Cost_Tracker::create_table() and writes psa_db_version.
*
* @return void
*/
function psa_maybe_run_upgrades() {
$stored = get_option( 'psa_db_version', '0.0.0' );
if ( version_compare( $stored, PSA_VERSION, '>=' ) ) {
if ( ! psa_upgrade_needed( $stored, PSA_VERSION ) ) {
return;
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* Tests for the schema-upgrade decision logic in peptide-search-ai.php.
*
* Covers psa_upgrade_needed() across the three version-comparison
* scenarios: fresh install, already-upgraded, and future-version.
*
* @package PeptideSearchAI
*/
declare( strict_types=1 );
use PHPUnit\Framework\TestCase;
// Stub WP hook registrars so requiring the plugin file does not explode.
if ( ! function_exists( 'add_shortcode' ) ) {
function add_shortcode() {}
}
if ( ! function_exists( 'register_activation_hook' ) ) {
function register_activation_hook() {}
}
if ( ! function_exists( 'register_deactivation_hook' ) ) {
function register_deactivation_hook() {}
}
// Load the plugin file once so the pure helper becomes available. The
// plugin's top-level side effects are limited to hook registrations, which
// are neutralized by the stubs above and the ones in bootstrap.php.
if ( ! function_exists( 'psa_upgrade_needed' ) ) {
require_once ABSPATH . 'peptide-search-ai.php';
}
/**
* Test case for psa_upgrade_needed().
*
* psa_upgrade_needed() is a pure function — no side effects, no WordPress
* API calls — so it can be exercised directly without a live option store.
*/
class UpgradeTest extends TestCase {
/**
* Fresh install: stored version defaults to '0.0.0', which is strictly
* less than any real released version, so an upgrade must run.
*/
public function test_fresh_install_triggers_upgrade(): void {
$this->assertTrue(
psa_upgrade_needed( '0.0.0', '4.4.3' ),
'Fresh install (stored = 0.0.0) must trigger an upgrade against any released version.'
);
}
/**
* Already-upgraded: stored version equals current. No upgrade needed.
*/
public function test_same_version_skips_upgrade(): void {
$this->assertFalse(
psa_upgrade_needed( '4.4.3', '4.4.3' ),
'Stored version equal to current version must not trigger an upgrade.'
);
}
/**
* Future-version: stored version is ahead of the running code (e.g., a
* downgrade, or a user running multiple sites out of sync). Do not run
* upgrade — it could apply an older schema over a newer one.
*/
public function test_future_stored_version_skips_upgrade(): void {
$this->assertFalse(
psa_upgrade_needed( '5.0.0', '4.4.3' ),
'Stored version greater than current version must not trigger an upgrade.'
);
}
/**
* Belt-and-braces: a strictly newer code version against an older
* stored version must trigger an upgrade. Guards against an accidental
* operator flip in version_compare() (e.g., '>=' vs '<').
*/
public function test_older_stored_version_triggers_upgrade(): void {
$this->assertTrue(
psa_upgrade_needed( '4.4.2', '4.4.3' ),
'Stored version older than current version must trigger an upgrade.'
);
}
}

View File

@@ -42,6 +42,7 @@ if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
// 3. Delete plugin options.
delete_option( 'psa_settings' );
delete_option( 'psa_search_cache_gen' );
delete_option( 'psa_db_version' );
// 4. Delete all plugin transients (validation cache, rate limits).
$wpdb->query(