feat(ci): adopt estate CI + add PHPUnit suite from scratch (v1.2.0)

- Add PHPUnit 9.6 test suite (WP stubs, tests: stubs) — 40 tests across 4 files:
  MathTest: 25 tests covering all PRC_Math public methods and calculate()
  orchestrator with real peptide scenarios (BPC-157/TB-500/Semaglutide/MK-677/
  Ipamorelin) plus edge cases (zero water, negative inputs, floor rounding,
  rounding precision, mcg↔mg conversion).
  DefaultPresetsTest: 11 tests — all 8 expected slugs, required keys per preset
  (dataProvider), dose_unit validity, min<max range, source field, spot-checks.
  RestControllerTest: 7 tests — calculate() 200 + correct values (mcg+mg),
  get_presets() 200, get_preset() 200 for known slug / WP_Error for unknown.
  PresetProviderTest: 4 tests — fallback to defaults sans PR Core, slug key
  present, cache invalidation, idempotent invalidation.
- Add phpunit.xml.dist + tests/bootstrap.php (WP function/class stubs).
- Add composer test script: vendor/bin/phpunit --configuration phpunit.xml.dist.
- Add phpunit/phpunit ^9.6 + yoast/phpunit-polyfills ^2.0 to require-dev.
- Replace standalone ci.yml with thin caller to estate reusable workflow:
  uses: peptiderepo/peptide-e2e/.github/workflows/ci.yml@main
  with: tests: stubs, has_js: true
  permissions: contents: write (required for phpcbf commit-back)
  workflow_call (required for deploy.yml gate).
- deploy.yml unchanged — already gates on ./ci.yml + uses shared deploy-app.yml.
- Version bump 1.1.0 → 1.2.0.

Agent-Session: claude/adopt-ci-20260616
This commit is contained in:
peptiderepo
2026-06-16 15:16:48 +00:00
parent 99b66c6370
commit 49934b2c25
10 changed files with 1357 additions and 66 deletions

331
tests/bootstrap.php Executable file
View File

@@ -0,0 +1,331 @@
<?php
/**
* PHPUnit bootstrap for Peptide Reconstitution Calculator tests.
*
* Loads WordPress stubs so unit tests can reference WP functions and classes
* without requiring a full WordPress installation. All stubs are minimal
* no-ops sufficient for the tested surface area.
*
* @package PRC\Tests
*/
// Composer autoloader.
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
// ── WordPress constant stubs ──────────────────────────────────────────
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', '/tmp/wordpress/' );
}
if ( ! defined( 'WPINC' ) ) {
define( 'WPINC', 'wp-includes' );
}
// Plugin constants normally defined in the main bootstrap file.
if ( ! defined( 'PRC_VERSION' ) ) {
define( 'PRC_VERSION', '1.2.0-test' );
}
if ( ! defined( 'PRC_PLUGIN_FILE' ) ) {
define( 'PRC_PLUGIN_FILE', dirname( __DIR__ ) . '/peptide-reconstitution-calculator.php' );
}
if ( ! defined( 'PRC_PLUGIN_DIR' ) ) {
define( 'PRC_PLUGIN_DIR', dirname( __DIR__ ) . '/' );
}
if ( ! defined( 'PRC_PLUGIN_URL' ) ) {
define( 'PRC_PLUGIN_URL', 'https://example.com/wp-content/plugins/peptide-reconstitution-calculator/' );
}
// ── WordPress function stubs ──────────────────────────────────────────
if ( ! function_exists( 'plugin_dir_path' ) ) {
function plugin_dir_path( $file ) {
return trailingslashit( dirname( $file ) );
}
}
if ( ! function_exists( 'plugin_dir_url' ) ) {
function plugin_dir_url( $file ) {
return 'https://example.com/wp-content/plugins/' . basename( dirname( $file ) ) . '/';
}
}
if ( ! function_exists( 'plugin_basename' ) ) {
function plugin_basename( $file ) {
return basename( dirname( $file ) ) . '/' . basename( $file );
}
}
if ( ! function_exists( 'trailingslashit' ) ) {
function trailingslashit( $string ) {
return rtrim( $string, '/\\' ) . '/';
}
}
if ( ! function_exists( 'sanitize_text_field' ) ) {
function sanitize_text_field( $str ) {
return trim( strip_tags( $str ) );
}
}
if ( ! function_exists( 'sanitize_title' ) ) {
function sanitize_title( $title, $fallback_title = '', $context = 'save' ) {
$title = strip_tags( $title );
$title = strtolower( $title );
$title = preg_replace( '/[^a-z0-9\-]/', '-', $title );
$title = preg_replace( '/-+/', '-', $title );
return trim( $title, '-' );
}
}
if ( ! function_exists( 'absint' ) ) {
function absint( $val ) {
return abs( (int) $val );
}
}
if ( ! function_exists( 'add_action' ) ) {
function add_action( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
return true;
}
}
if ( ! function_exists( 'add_filter' ) ) {
function add_filter( $tag, $callback, $priority = 10, $accepted_args = 1 ) {
return true;
}
}
if ( ! function_exists( 'add_shortcode' ) ) {
function add_shortcode( $tag, $callback ) {
return true;
}
}
if ( ! function_exists( 'shortcode_atts' ) ) {
function shortcode_atts( $pairs, $atts, $shortcode = '' ) {
$atts = (array) $atts;
$out = array();
foreach ( $pairs as $name => $default ) {
$out[ $name ] = array_key_exists( $name, $atts ) ? $atts[ $name ] : $default;
}
return $out;
}
}
if ( ! function_exists( 'register_rest_route' ) ) {
function register_rest_route( $namespace, $route, $args = array(), $override = false ) {
return true;
}
}
if ( ! function_exists( 'rest_url' ) ) {
function rest_url( $path = '' ) {
return 'https://example.com/wp-json/' . ltrim( $path, '/' );
}
}
if ( ! function_exists( 'esc_url_raw' ) ) {
function esc_url_raw( $url, $protocols = null ) {
return filter_var( $url, FILTER_SANITIZE_URL ) ?: '';
}
}
if ( ! function_exists( 'esc_attr' ) ) {
function esc_attr( $text ) {
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'esc_html' ) ) {
function esc_html( $text ) {
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'esc_html__' ) ) {
function esc_html__( $text, $domain = 'default' ) {
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'esc_html_e' ) ) {
function esc_html_e( $text, $domain = 'default' ) {
echo htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( '__' ) ) {
function __( $text, $domain = 'default' ) {
return $text;
}
}
if ( ! function_exists( 'is_admin' ) ) {
function is_admin() {
global $_test_is_admin;
return ! empty( $_test_is_admin );
}
}
if ( ! function_exists( 'wp_enqueue_style' ) ) {
function wp_enqueue_style( $handle, $src = '', $deps = array(), $ver = false, $media = 'all' ) {
return true;
}
}
if ( ! function_exists( 'wp_enqueue_script' ) ) {
function wp_enqueue_script( $handle, $src = '', $deps = array(), $ver = false, $args = array() ) {
return true;
}
}
if ( ! function_exists( 'wp_localize_script' ) ) {
function wp_localize_script( $handle, $object_name, $l10n ) {
return true;
}
}
if ( ! function_exists( 'get_transient' ) ) {
function get_transient( $transient ) {
global $_test_transients;
return isset( $_test_transients[ $transient ] ) ? $_test_transients[ $transient ] : false;
}
}
if ( ! function_exists( 'set_transient' ) ) {
function set_transient( $transient, $value, $expiration = 0 ) {
global $_test_transients;
$_test_transients[ $transient ] = $value;
return true;
}
}
if ( ! function_exists( 'delete_transient' ) ) {
function delete_transient( $transient ) {
global $_test_transients;
unset( $_test_transients[ $transient ] );
return true;
}
}
if ( ! function_exists( 'register_activation_hook' ) ) {
function register_activation_hook( $file, $callback ) {
return true;
}
}
if ( ! function_exists( 'register_deactivation_hook' ) ) {
function register_deactivation_hook( $file, $callback ) {
return true;
}
}
if ( ! function_exists( 'delete_option' ) ) {
function delete_option( $option ) {
global $_test_options;
unset( $_test_options[ $option ] );
return true;
}
}
if ( ! function_exists( 'wp_cache_flush' ) ) {
function wp_cache_flush() {
return true;
}
}
if ( ! function_exists( 'get_option' ) ) {
function get_option( $option, $default = false ) {
global $_test_options;
return isset( $_test_options[ $option ] ) ? $_test_options[ $option ] : $default;
}
}
if ( ! function_exists( 'do_action' ) ) {
function do_action( $tag, ...$args ) {
return null;
}
}
// WP_REST_Request stub used by REST controller tests.
if ( ! class_exists( 'WP_REST_Request' ) ) {
class WP_REST_Request {
/** @var array<string, mixed> */
private array $params = array();
private string $method;
public function __construct( string $method = 'GET', string $route = '' ) {
$this->method = $method;
}
/**
* @param string $key
* @param mixed $value
*/
public function set_param( string $key, $value ): void {
$this->params[ $key ] = $value;
}
/**
* @param string $key
* @return mixed
*/
public function get_param( string $key ) {
return $this->params[ $key ] ?? null;
}
/** @return array<string, mixed> */
public function get_params(): array {
return $this->params;
}
}
}
// WP_REST_Response stub.
if ( ! class_exists( 'WP_REST_Response' ) ) {
class WP_REST_Response {
/** @var mixed */
public $data;
public int $status;
/**
* @param mixed $data
*/
public function __construct( $data = null, int $status = 200 ) {
$this->data = $data;
$this->status = $status;
}
/** @return mixed */
public function get_data() {
return $this->data;
}
public function get_status(): int {
return $this->status;
}
}
}
// WP_Error stub.
if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
/** @var array<string, string[]> */
public array $errors = array();
/** @var array<string, mixed> */
public array $error_data = array();
/**
* @param mixed $data
*/
public function __construct( string $code = '', string $message = '', $data = '' ) {
if ( $code ) {
$this->errors[ $code ][] = $message;
if ( $data ) {
$this->error_data[ $code ] = $data;
}
}
}
public function get_error_code(): string {
$codes = array_keys( $this->errors );
return $codes ? $codes[0] : '';
}
public function get_error_message( string $code = '' ): string {
if ( ! $code ) {
$code = $this->get_error_code();
}
return isset( $this->errors[ $code ] ) ? $this->errors[ $code ][0] : '';
}
}
}
if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $thing ): bool {
return ( $thing instanceof WP_Error );
}
}
// ── Load plugin classes for testing ──────────────────────────────────
// The autoloader resolves PRC_ classes from includes/ — register it now.
require_once PRC_PLUGIN_DIR . 'includes/class-prc-autoloader.php';
PRC_Autoloader::register();
// Load pure classes explicitly (no WP hooks fire at require time).
require_once PRC_PLUGIN_DIR . 'includes/class-prc-math.php';
require_once PRC_PLUGIN_DIR . 'includes/class-prc-default-presets.php';
require_once PRC_PLUGIN_DIR . 'includes/class-prc-calculator.php';
require_once PRC_PLUGIN_DIR . 'includes/class-prc-preset-provider.php';
require_once PRC_PLUGIN_DIR . 'includes/api/class-prc-rest-controller.php';
require_once PRC_PLUGIN_DIR . 'includes/frontend/class-prc-shortcode.php';