Files
2meet-data-optimizer/tests/integration/PostShadowVerifierTest.php
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer v0.1.0
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 05:06:36 +08:00

331 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Integration test: WPDO_Post_Shadow_Verifier (v2.10.3).
*
* Verifies sample-and-compare logic between flat tables and wp_postmeta.
* Result tuple: {sampled, matched, diffs, missing_flat, missing_postmeta}.
*
* Logger integration is exercised via WPDO_Shadow_Diff_Logger; this test
* focuses on the verifier's sampling + counting contract.
*/
class PostShadowVerifierTest extends TestCase {
private const POSTS = 'wp_itest_posts';
private const POSTMETA = 'wp_itest_postmeta';
private const FLAT = 'wp_itest_wpdo_post_wc_product';
public static function setUpBeforeClass(): void {
global $wpdb;
if ( ! interface_exists( 'WPDO_Entity_Adapter_Interface' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/adapters/interface-entity-adapter.php';
}
foreach ( array(
'includes/engine/class-tmdo-entity-registry.php',
'includes/engine/class-tmdo-mode-manager.php',
'includes/engine/class-tmdo-schema-manager.php',
'includes/adapters/class-tmdo-adapter-post.php',
'includes/integrations/class-tmdo-post-fields.php',
'includes/class-tmdo-post-shadow-verifier.php',
) as $rel ) {
$file = WPDO_PLUGIN_DIR . $rel;
if ( file_exists( $file ) ) {
require_once $file;
}
}
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' );
$wpdb->query(
'CREATE TABLE `' . self::POSTS . '` (
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_type varchar(20) NOT NULL DEFAULT \'post\',
post_status varchar(20) NOT NULL DEFAULT \'publish\',
PRIMARY KEY (ID),
KEY post_type (post_type)
) DEFAULT CHARACTER SET utf8mb4'
);
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' );
$wpdb->query(
'CREATE TABLE `' . self::POSTMETA . '` (
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_id bigint(20) unsigned NOT NULL DEFAULT 0,
meta_key varchar(255) DEFAULT NULL,
meta_value longtext,
PRIMARY KEY (meta_id),
KEY post_id (post_id),
KEY meta_key (meta_key(191))
) DEFAULT CHARACTER SET utf8mb4'
);
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' );
$wpdb->query(
'CREATE TABLE `' . self::FLAT . '` (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_id bigint(20) unsigned NOT NULL,
_price decimal(18,6) DEFAULT NULL,
_stock_status varchar(100) DEFAULT NULL,
_sku longtext DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_post_id (post_id)
) DEFAULT CHARACTER SET utf8mb4'
);
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
WPDO_Post_Fields::register_entity_fields();
}
public static function tearDownAfterClass(): void {
global $wpdb;
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' );
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' );
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' );
WPDO_Entity_Registry::init();
}
protected function setUp(): void {
global $wpdb;
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTS . '`' );
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
$wpdb->query( 'TRUNCATE TABLE `' . self::FLAT . '`' );
}
private function seed_consistent( int $post_id, string $price, string $stock ): void {
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $price ) );
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_stock_status', 'meta_value' => $stock ) );
$wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $price, '_stock_status' => $stock ) );
}
private function seed_diverged( int $post_id, string $pm_price, string $flat_price ): void {
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $pm_price ) );
$wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $flat_price ) );
}
// ── sample_compare() ─────────────────────────────────────────────────────
public function test_returns_expected_shape(): void {
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price', '_stock_status' ),
5
);
$this->assertArrayHasKey( 'sampled', $result );
$this->assertArrayHasKey( 'matched', $result );
$this->assertArrayHasKey( 'diffs', $result );
$this->assertArrayHasKey( 'missing_flat', $result );
$this->assertArrayHasKey( 'missing_postmeta', $result );
}
public function test_all_match_when_data_is_consistent(): void {
for ( $i = 1; $i <= 5; $i++ ) {
$this->seed_consistent( $i, '50.00', 'instock' );
}
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
5
);
$this->assertSame( 5, $result['sampled'] );
$this->assertSame( 5, $result['matched'] );
$this->assertSame( 0, $result['diffs'] );
$this->assertSame( 0, $result['missing_flat'] );
}
public function test_detects_divergence_between_flat_and_postmeta(): void {
// Two diverged: pm has 50, flat has 60.
$this->seed_diverged( 1, '50', '60' );
$this->seed_diverged( 2, '99', '88' );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
5
);
$this->assertSame( 2, $result['sampled'] );
$this->assertSame( 0, $result['matched'] );
$this->assertSame( 2, $result['diffs'] );
}
public function test_counts_missing_flat_when_flat_row_absent(): void {
// Post + postmeta exist, but no flat row.
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => 100, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array( 'post_id' => 100, 'meta_key' => '_price', 'meta_value' => '99' ) );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
5
);
$this->assertSame( 1, $result['sampled'] );
$this->assertSame( 1, $result['missing_flat'] );
}
public function test_counts_missing_postmeta_when_pm_absent(): void {
// Post + flat exist, but no postmeta.
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => 200, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::FLAT, array( 'post_id' => 200, '_price' => '50' ) );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
5
);
$this->assertSame( 1, $result['sampled'] );
$this->assertSame( 1, $result['missing_postmeta'] );
}
public function test_returns_zero_when_no_posts_of_type(): void {
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
5
);
$this->assertSame( 0, $result['sampled'] );
$this->assertSame( 0, $result['matched'] );
}
public function test_caps_sample_at_available_post_count(): void {
// 3 posts, ask for 10 samples → only 3 sampled.
$this->seed_consistent( 1, '10', 'instock' );
$this->seed_consistent( 2, '20', 'instock' );
$this->seed_consistent( 3, '30', 'instock' );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_price' ),
10
);
$this->assertSame( 3, $result['sampled'] );
$this->assertSame( 3, $result['matched'] );
}
public function test_rejects_zero_sample_size(): void {
$this->expectException( InvalidArgumentException::class );
WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 0 );
}
public function test_rejects_empty_keys(): void {
$this->expectException( InvalidArgumentException::class );
WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array(), 5 );
}
// ── v2.10.5: serialize vs JSON loose equality ────────────────────────────
public function test_treats_serialized_array_equal_to_json_array(): void {
// pm side has serialized array; flat side has JSON for the same data.
// post_id=10, key=_stock_status (we reuse this column to inject test values).
// Use a dedicated key for clarity by re-purposing the keys array.
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => 10, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array(
'post_id' => 10,
'meta_key' => '_sku',
'meta_value' => serialize( array( 'a', 'b', 'c' ) ),
) );
$wpdb->insert( self::FLAT, array(
'post_id' => 10,
'_sku' => wp_json_encode( array( 'a', 'b', 'c' ) ),
) );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_sku' ),
5
);
$this->assertSame(
1,
$result['matched'],
'serialized array vs JSON-encoded same array should match.'
);
$this->assertSame( 0, $result['diffs'], 'No false-positive diff.' );
}
public function test_treats_serialized_assoc_equal_to_json_assoc(): void {
global $wpdb;
$assoc = array( 'width' => 100, 'height' => 200 );
$wpdb->insert( self::POSTS, array( 'ID' => 11, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array(
'post_id' => 11,
'meta_key' => '_sku',
'meta_value' => serialize( $assoc ),
) );
$wpdb->insert( self::FLAT, array(
'post_id' => 11,
'_sku' => wp_json_encode( $assoc ),
) );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_sku' ),
5
);
$this->assertSame( 1, $result['matched'] );
$this->assertSame( 0, $result['diffs'] );
}
public function test_genuine_diff_still_detected_after_loose_equal_widening(): void {
// Real divergence — must still be flagged even with loosened compare.
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => 12, 'post_type' => 'product', 'post_status' => 'publish' ) );
$wpdb->insert( self::POSTMETA, array(
'post_id' => 12,
'meta_key' => '_sku',
'meta_value' => serialize( array( 1, 2, 3 ) ),
) );
$wpdb->insert( self::FLAT, array(
'post_id' => 12,
'_sku' => wp_json_encode( array( 9, 9, 9 ) ),
) );
$result = WPDO_Post_Shadow_Verifier::sample_compare(
'product',
'wc_product',
self::FLAT,
array( '_sku' ),
5
);
$this->assertSame( 0, $result['matched'] );
$this->assertSame( 1, $result['diffs'] );
}
}