feat: add browsable peptide directory with category filtering, REST API, and extended meta fields (v4.3.0)

Adds [peptide_directory] shortcode with card grid, category taxonomy (8 terms),
search, pagination, detail modal, and /v1/compounds REST endpoint. Includes 9
new meta fields (half-life, stability, solubility, etc.), admin data tools for
migration/re-enrichment, integration hooks for cross-plugin extensibility,
dark mode support, and full uninstall cleanup. Refactors AI generator into
PSA_OpenRouter + PSA_KB_Builder classes. Adds PHPUnit tests for PostType and
Directory classes. Fixes QA findings: dead JS selector, renderError loading
state, numeric meta validation, prepared SQL in uninstall.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
peptiderepo
2026-04-13 21:30:11 +07:00
parent 0fa2707163
commit ed65b6ab90
28 changed files with 3380 additions and 783 deletions

136
tests/unit/DirectoryTest.php Executable file
View File

@@ -0,0 +1,136 @@
<?php
/**
* Tests for PSA_Directory class.
*
* Covers: constants, format_compound() output structure.
*
* @package PeptideSearchAI
*/
declare( strict_types=1 );
// Stub WordPress functions needed by PSA_Directory.
if ( ! function_exists( 'add_shortcode' ) ) {
function add_shortcode() {}
}
if ( ! function_exists( 'add_action' ) ) {
function add_action() {}
}
if ( ! function_exists( 'register_rest_route' ) ) {
function register_rest_route() {}
}
if ( ! function_exists( 'get_permalink' ) ) {
function get_permalink( $id ) {
return 'https://example.com/peptides/test-peptide/';
}
}
if ( ! function_exists( 'wp_get_object_terms' ) ) {
function wp_get_object_terms( $id, $taxonomy, $args = array() ) {
return array();
}
}
if ( ! function_exists( 'get_post_meta' ) ) {
function get_post_meta( $post_id, $key = '', $single = false ) {
$meta = array(
'psa_half_life' => '4-6 hours',
'psa_stability' => '28 days at 2-8°C',
'psa_source' => 'ai-generated',
'psa_sequence' => 'Gly-Glu-Pro',
'psa_molecular_weight' => '1419.53 Da',
'psa_molecular_formula' => 'C62H98N16O22',
'psa_aliases' => 'BPC-157, Pentadecapeptide',
'psa_mechanism' => 'Test mechanism',
'psa_research_apps' => 'Test research',
'psa_safety_profile' => 'Test safety',
'psa_dosage_info' => 'Test dosage',
'psa_references' => 'Test refs',
'psa_pubchem_cid' => '12345',
'psa_solubility' => 'Bacteriostatic Water',
'psa_vial_size_mg' => '5',
'psa_storage_lyophilized' => '-20°C',
'psa_storage_reconstituted' => '2-8°C',
'psa_typical_dose_mcg' => '200-300 mcg',
'psa_cycle_parameters' => '4-6 weeks',
'psa_amino_acid_count' => '15',
);
return $meta[ $key ] ?? '';
}
}
if ( ! function_exists( 'wp_trim_words' ) ) {
function wp_trim_words( $text, $num_words = 55 ) {
$words = explode( ' ', $text );
return implode( ' ', array_slice( $words, 0, $num_words ) );
}
}
if ( ! function_exists( '__' ) ) {
function __( $text, $domain = 'default' ) {
return $text;
}
}
require_once dirname( __DIR__ ) . '/bootstrap.php';
require_once ABSPATH . 'includes/class-psa-post-type.php';
require_once ABSPATH . 'includes/class-psa-directory.php';
class DirectoryTest extends \PHPUnit\Framework\TestCase {
/**
* Verify directory pagination constants.
*/
public function test_per_page_constants(): void {
$this->assertSame( 12, PSA_Directory::PER_PAGE_DEFAULT );
$this->assertSame( 100, PSA_Directory::PER_PAGE_MAX );
}
/**
* Verify format_compound() returns expected keys for full field set.
*/
public function test_format_compound_full_fields(): void {
$mock_post = new \stdClass();
$mock_post->ID = 42;
$mock_post->post_title = 'BPC-157';
$mock_post->post_name = 'bpc-157';
$mock_post->post_content = 'A synthetic peptide for research purposes.';
$result = PSA_Directory::format_compound( $mock_post, 'full' );
// Check required top-level keys.
$required_keys = array(
'id', 'name', 'slug', 'url', 'categories', 'half_life', 'stability',
'source', 'extras', 'excerpt', 'description', 'sequence',
'molecular_weight', 'molecular_formula', 'aliases', 'mechanism',
'pubchem_cid', 'solubility', 'vial_size_mg', 'storage_lyophilized',
'storage_reconstituted', 'typical_dose_mcg', 'cycle_parameters',
'amino_acid_count',
);
foreach ( $required_keys as $key ) {
$this->assertArrayHasKey( $key, $result, "Missing key in full compound: {$key}" );
}
$this->assertSame( 42, $result['id'] );
$this->assertSame( 'BPC-157', $result['name'] );
$this->assertSame( 'bpc-157', $result['slug'] );
}
/**
* Verify format_compound() with 'basic' fields returns lightweight data.
*/
public function test_format_compound_basic_fields(): void {
$mock_post = new \stdClass();
$mock_post->ID = 42;
$mock_post->post_title = 'BPC-157';
$mock_post->post_name = 'bpc-157';
$mock_post->post_content = 'A synthetic peptide.';
$result = PSA_Directory::format_compound( $mock_post, 'basic' );
// Basic should NOT include detail fields.
$this->assertArrayHasKey( 'id', $result );
$this->assertArrayHasKey( 'name', $result );
$this->assertArrayHasKey( 'vial_size_mg', $result );
$this->assertArrayNotHasKey( 'sequence', $result );
$this->assertArrayNotHasKey( 'mechanism', $result );
$this->assertArrayNotHasKey( 'description', $result );
}
}