Files
peptide-repo-core/tests/bootstrap.php
peptiderepo 1ce471981b feat: PRAB article JSON-LD emitter + meta reader (v0.8.0)
- PR_Core_Jsonld_Article: meta-triggered emitter for PRAutoBlogger posts
  (trigger: _prab_schema_version=1 on post type). Yoast path retypes page
  node to MedicalWebPage (wpseo_schema_webpage_type priority 10) and enriches
  Article piece with citation[], about[]→Drug @id, lastReviewed, honest
  reviewedBy (wpseo_schema_graph priority 13). No-Yoast fallback emits
  standalone @graph. Posts without trigger meta are byte-unchanged.

- PR_Core_Prab_Meta_Reader: sanitises all _prab_* contract-v1 meta at read
  time. URLs validated (http/https), DOIs normalised to https://doi.org/ URI,
  JSON arrays per-element validated, WP user ID resolved. Malformed values
  degrade gracefully. Honest reviewedBy: Person ONLY when mode=human + valid
  user ID; else Organization. Never a fabricated person.

- Unit tests: JsonldArticleTest.php — 22 assertions covering is_triggered,
  review_mode, reviewed_by Person/Org decision tree, citation sanitization,
  DOI normalisation, about linkage, retype passthrough, enrich, no-new-nodes,
  editorial-system-never-person.

- bootstrap.php: WP_User stub + get_userdata reads pr_test_userdata global.
- ARCHITECTURE.md: updated JSON-LD class tree + PRAB emission section.
- CONVENTIONS.md: PRAB contract v2 extension guide added.
- CHANGELOG.md + version bump to v0.8.0.

Ratified contract: convo/prcore/decisions/2026-06-11-jsonld-contract-v1.md
File sizes: article.php=235L, meta-reader.php=265L (both < 300-line gate).
No duplicate Article/WebPage nodes. uninstall.php unaffected (_prab_* unowned).

Agent-Session: prcore-jsonld-article-20260622
2026-06-23 04:23:21 +00:00

438 lines
14 KiB
PHP

<?php
/**
* PHPUnit bootstrap for Peptide Repo Core tests.
*
* Loads WordPress stubs so unit tests can reference WP functions/classes
* without requiring a full WordPress installation.
*
* @package PeptideRepoCore\Tests
*/
declare(strict_types=1);
// Composer autoloader.
require_once dirname( __DIR__ ) . '/vendor/autoload.php';
// ── WordPress constants ───────────────────────────────────────────────.
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', '/tmp/wordpress/' );
}
if ( ! defined( 'WPINC' ) ) {
define( 'WPINC', 'wp-includes' );
}
if ( ! defined( 'PR_CORE_VERSION' ) ) {
define( 'PR_CORE_VERSION', '0.7.0-test' );
}
if ( ! defined( 'PR_CORE_PLUGIN_DIR' ) ) {
define( 'PR_CORE_PLUGIN_DIR', dirname( __DIR__ ) . '/' );
}
if ( ! defined( 'PR_CORE_PLUGIN_URL' ) ) {
define( 'PR_CORE_PLUGIN_URL', 'https://example.com/wp-content/plugins/peptide-repo-core/' );
}
if ( ! defined( 'PR_CORE_PLUGIN_FILE' ) ) {
define( 'PR_CORE_PLUGIN_FILE', PR_CORE_PLUGIN_DIR . 'peptide-repo-core.php' );
}
if ( ! defined( 'PR_CORE_TARGET_SCHEMA_VERSION' ) ) {
define( 'PR_CORE_TARGET_SCHEMA_VERSION', 4 );
}
if ( ! defined( 'DAY_IN_SECONDS' ) ) {
define( 'DAY_IN_SECONDS', 86400 );
}
if ( ! defined( 'HOUR_IN_SECONDS' ) ) {
define( 'HOUR_IN_SECONDS', 3600 );
}
if ( ! defined( 'MINUTE_IN_SECONDS' ) ) {
define( 'MINUTE_IN_SECONDS', 60 );
}
// ── Global harness state (reset between test cases via setUp) ─────────.
$GLOBALS['pr_core_test_state'] = [
'existing_post_types' => [],
'existing_taxonomies' => [],
'registered_post_types' => [],
'registered_taxonomies' => [],
'registered_meta' => [],
'added_actions' => [],
'added_filters' => [],
];
$GLOBALS['pr_core_options'] = [];
$GLOBALS['pr_core_cron_calls'] = [];
$GLOBALS['pr_test_postmeta'] = [];
$GLOBALS['pr_test_posts'] = [];
$GLOBALS['pr_test_userdata'] = [];
$GLOBALS['pr_test_updated_meta'] = [];
$GLOBALS['pr_test_is_singular'] = false;
$GLOBALS['pr_test_singular_type'] = '';
$GLOBALS['pr_test_the_id'] = 0;
$GLOBALS['pr_test_peptide_dto'] = null;
// ── WordPress function stubs ──────────────────────────────────────────.
if ( ! function_exists( 'trailingslashit' ) ) {
function trailingslashit( string $path ): string {
return rtrim( $path, '/\\' ) . '/';
}
}
if ( ! function_exists( 'plugin_dir_path' ) ) {
function plugin_dir_path( string $file ): string {
return trailingslashit( dirname( $file ) );
}
}
if ( ! function_exists( 'plugin_dir_url' ) ) {
function plugin_dir_url( string $file ): string {
return 'https://example.com/wp-content/plugins/' . basename( dirname( $file ) ) . '/';
}
}
if ( ! function_exists( 'plugin_basename' ) ) {
function plugin_basename( string $file ): string {
return basename( dirname( $file ) ) . '/' . basename( $file );
}
}
if ( ! function_exists( '__' ) ) {
function __( string $text, string $domain = 'default' ): string {
return $text;
}
}
if ( ! function_exists( 'esc_html' ) ) {
function esc_html( string $text ): string {
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'esc_html__' ) ) {
function esc_html__( string $text, string $domain = 'default' ): string {
return esc_html( $text );
}
}
if ( ! function_exists( 'esc_attr' ) ) {
function esc_attr( string $text ): string {
return htmlspecialchars( $text, ENT_QUOTES, 'UTF-8' );
}
}
if ( ! function_exists( 'esc_url_raw' ) ) {
function esc_url_raw( string $url ): string {
return filter_var( $url, FILTER_SANITIZE_URL ) ?: '';
}
}
if ( ! function_exists( 'sanitize_text_field' ) ) {
function sanitize_text_field( $value ): string {
return is_scalar( $value ) ? trim( strip_tags( (string) $value ) ) : '';
}
}
if ( ! function_exists( 'sanitize_textarea_field' ) ) {
function sanitize_textarea_field( $value ): string {
return is_scalar( $value ) ? trim( strip_tags( (string) $value ) ) : '';
}
}
if ( ! function_exists( 'wp_strip_all_tags' ) ) {
function wp_strip_all_tags( $value ): string {
return strip_tags( (string) $value );
}
}
if ( ! function_exists( 'absint' ) ) {
function absint( $value ): int {
return abs( (int) $value );
}
}
if ( ! function_exists( 'wp_json_encode' ) ) {
function wp_json_encode( $data, int $flags = 0, int $depth = 512 ): string {
return json_encode( $data, $flags, $depth ) ?: '[]';
}
}
if ( ! function_exists( 'is_wp_error' ) ) {
function is_wp_error( $thing ): bool {
return $thing instanceof WP_Error;
}
}
if ( ! function_exists( 'apply_filters' ) ) {
function apply_filters( string $tag, $value ) {
return $value;
}
}
if ( ! function_exists( 'current_user_can' ) ) {
function current_user_can( string $capability ): bool {
return true;
}
}
if ( ! function_exists( 'home_url' ) ) {
function home_url( string $path = '' ): string {
return 'https://peptiderepo.com' . $path;
}
}
if ( ! function_exists( 'error_log' ) ) {
function error_log( string $msg ): bool {
return true;
}
}
if ( ! function_exists( 'is_email' ) ) {
function is_email( string $email ): bool {
return (bool) filter_var( $email, FILTER_VALIDATE_EMAIL );
}
}
// ── Option / cron stubs ───────────────────────────────────────────────.
if ( ! function_exists( 'get_option' ) ) {
function get_option( string $key, $default = false ) {
if ( isset( $GLOBALS['pr_core_options'][ $key ] ) ) {
return $GLOBALS['pr_core_options'][ $key ];
}
return $default;
}
}
if ( ! function_exists( 'update_option' ) ) {
function update_option( string $key, $value ): bool {
$GLOBALS['pr_core_options'][ $key ] = $value;
return true;
}
}
if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) {
function wp_clear_scheduled_hook( string $hook ): void {
$GLOBALS['pr_core_cron_calls'][] = [ 'action' => 'clear', 'hook' => $hook ];
}
}
if ( ! function_exists( 'wp_schedule_event' ) ) {
function wp_schedule_event( int $timestamp, string $recurrence, string $hook ): void {
$GLOBALS['pr_core_cron_calls'][] = [
'action' => 'schedule',
'hook' => $hook,
'recurrence' => $recurrence,
];
}
}
if ( ! function_exists( 'register_setting' ) ) {
function register_setting( string $group, string $option, array $args = [] ): void {}
}
if ( ! function_exists( 'add_settings_section' ) ) {
function add_settings_section( string $id, string $title, $cb, string $page ): void {}
}
if ( ! function_exists( 'add_settings_field' ) ) {
function add_settings_field( string $id, string $title, $cb, string $page, string $section = '' ): void {}
}
// ── Post meta stubs ───────────────────────────────────────────────────.
if ( ! function_exists( 'get_post_meta' ) ) {
function get_post_meta( $post_id, string $key = '', bool $single = false ) {
if ( $single ) {
return $GLOBALS['pr_test_postmeta'][ (int) $post_id ][ $key ] ?? '';
}
return array_values( $GLOBALS['pr_test_postmeta'][ (int) $post_id ] ?? [] );
}
}
if ( ! function_exists( 'update_post_meta' ) ) {
function update_post_meta( int $post_id, string $key, $value ): bool {
$GLOBALS['pr_test_postmeta'][ $post_id ][ $key ] = $value;
$GLOBALS['pr_test_updated_meta'][] = [
'post_id' => $post_id,
'key' => $key,
'value' => $value,
];
return true;
}
}
if ( ! function_exists( 'get_post' ) ) {
function get_post( int $id ) {
return $GLOBALS['pr_test_posts'][ $id ] ?? null;
}
}
if ( ! function_exists( 'get_posts' ) ) {
function get_posts( array $args = [] ): array {
return $GLOBALS['pr_test_all_post_ids'] ?? [];
}
}
// ── CPT / taxonomy stubs ──────────────────────────────────────────────.
if ( ! function_exists( 'post_type_exists' ) ) {
function post_type_exists( string $post_type ): bool {
return in_array( $post_type, $GLOBALS['pr_core_test_state']['existing_post_types'], true );
}
}
if ( ! function_exists( 'taxonomy_exists' ) ) {
function taxonomy_exists( string $taxonomy ): bool {
return in_array( $taxonomy, $GLOBALS['pr_core_test_state']['existing_taxonomies'], true );
}
}
if ( ! function_exists( 'register_post_type' ) ) {
function register_post_type( string $post_type, array $args = [] ) {
$GLOBALS['pr_core_test_state']['registered_post_types'][ $post_type ] = $args;
$GLOBALS['pr_core_test_state']['existing_post_types'][] = $post_type;
return (object) [ 'name' => $post_type, 'args' => $args ];
}
}
if ( ! function_exists( 'register_taxonomy' ) ) {
function register_taxonomy( string $taxonomy, $object_type, array $args = [] ): void {
$GLOBALS['pr_core_test_state']['registered_taxonomies'][ $taxonomy ] = [
'object_type' => $object_type,
'args' => $args,
];
$GLOBALS['pr_core_test_state']['existing_taxonomies'][] = $taxonomy;
}
}
if ( ! function_exists( 'register_post_meta' ) ) {
function register_post_meta( string $post_type, string $key, array $args = [] ): bool {
$GLOBALS['pr_core_test_state']['registered_meta'][ $post_type ][ $key ] = $args;
return true;
}
}
if ( ! function_exists( 'add_action' ) ) {
function add_action( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
$GLOBALS['pr_core_test_state']['added_actions'][] = [
'hook' => $hook,
'callback' => $callback,
'priority' => $priority,
];
return true;
}
}
if ( ! function_exists( 'add_filter' ) ) {
function add_filter( string $hook, $callback, int $priority = 10, int $accepted_args = 1 ): bool {
$GLOBALS['pr_core_test_state']['added_filters'][] = [
'hook' => $hook,
'callback' => $callback,
'priority' => $priority,
];
return true;
}
}
// ── Singular / permalink stubs ────────────────────────────────────────.
if ( ! function_exists( 'is_singular' ) ) {
function is_singular( $type = '' ): bool {
if ( '' === $type ) {
return $GLOBALS['pr_test_is_singular'];
}
return $GLOBALS['pr_test_is_singular']
&& ( $GLOBALS['pr_test_singular_type'] === $type );
}
}
if ( ! function_exists( 'get_the_ID' ) ) {
function get_the_ID(): int {
return $GLOBALS['pr_test_the_id'];
}
}
if ( ! function_exists( 'get_permalink' ) ) {
function get_permalink( $post_id = null ): string {
return 'https://peptiderepo.com/peptides/bpc-157/';
}
}
if ( ! function_exists( 'get_post_modified_time' ) ) {
function get_post_modified_time( $format, $gmt = false, $post = null ) {
return '2026-04-25';
}
}
if ( ! function_exists( 'get_userdata' ) ) {
function get_userdata( int $id ) {
$data = $GLOBALS['pr_test_userdata'][ $id ] ?? null;
if ( null !== $data ) {
return $data instanceof WP_User ? $data : new WP_User( (array) $data );
}
return false;
}
}
if ( ! class_exists( 'WP_User' ) ) {
class WP_User {
public int $ID = 0;
public string $display_name = '';
public string $user_login = '';
public string $user_email = '';
/** @param array<string, mixed> $data */
public function __construct( array $data = [] ) {
foreach ( $data as $key => $value ) {
$this->$key = $value;
}
}
}
}
if ( ! function_exists( 'get_author_posts_url' ) ) {
function get_author_posts_url( int $id ): string {
return 'https://peptiderepo.com/author/' . $id;
}
}
// ── WP_Error stub ─────────────────────────────────────────────────────.
if ( ! class_exists( 'WP_Error' ) ) {
class WP_Error {
/** @var array<string, string[]> */
public array $errors = [];
/** @var array<string, mixed> */
public array $error_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] : '';
}
}
}
// ── WP_Post stub ──────────────────────────────────────────────────────.
if ( ! class_exists( 'WP_Post' ) ) {
class WP_Post {
public int $ID = 0;
public string $post_author = '0';
public string $post_date = '';
public string $post_title = '';
public string $post_content = '';
public string $post_excerpt = '';
public string $post_status = 'publish';
public string $post_name = '';
public string $post_type = 'post';
public string $post_modified = '';
/**
* @param array<string, mixed>|object $data
*/
public function __construct( $data = [] ) {
foreach ( (array) $data as $key => $value ) {
$this->$key = $value;
}
}
}
}
if ( ! function_exists( 'wp_get_post_terms' ) ) {
function wp_get_post_terms( int $post_id, string $taxonomy, array $args = [] ): array {
return [];
}
}
if ( ! function_exists( 'sanitize_title' ) ) {
function sanitize_title( string $title ): string {
return strtolower( trim( preg_replace( '/[^a-z0-9-]/', '-', strtolower( $title ) ) ?: '', '-' ) );
}
}
if ( ! function_exists( 'wp_count_posts' ) ) {
function wp_count_posts( string $type = 'post' ) {
return (object) [ 'publish' => 0 ];
}
}
// ── Load the plugin's autoloader ──────────────────────────────────────.
require_once PR_CORE_PLUGIN_DIR . 'includes/class-pr-core-autoloader.php';
PR_Core_Autoloader::register();