feat(2meet-data-optimizer): guard orphaned flat-table groups

- On init:1, re-register aeav_only groups whose flat table exists and whose definition is in wpdo_registry_meta but no active plugin registers them, so their data stays readable (routing only, never DDL)
- Keys owned by a live group, or with rows in native meta (newer data written while orphaned), are left alone; the native-meta check is cached for a day
- Surface restored groups via Site Health (wpdo_orphaned_entity_groups), the admin Doctor tab, and wp wpdo doctor
- Verify: EntityRegistryRestoreTest (9), OrphanGroupRestoreTest (7); core unit 622 / integration 439, HP 141/49, WC 10/22 OK; dry-run on dev20–22 restores 16/17/4 groups, keeps hp_taxonomy hp_sort_order/hp_icon on native
- PR: PR-018
This commit is contained in:
2026-09-25 23:38:12 +08:00
parent e9d8696c93
commit 1653011f4f
9 changed files with 583 additions and 20 deletions
+116
View File
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Restoring orphaned entity groups from their persisted definition.
*
* When the plugin that registered a group is gone, its flat table keeps the
* data but nothing maps the meta keys to it any more — in aeav_only mode the
* values read back as empty. restore_group() re-registers the stored
* definition for read/write routing only: it must never queue DDL and must
* not take over keys another live group already manages.
*/
class EntityRegistryRestoreTest extends TestCase {
protected function setUp(): void {
if ( ! class_exists( 'TMDO_Site_Health' ) ) {
require_once TMDO_PATH . 'includes/diagnostic/class-tmdo-site-health.php';
}
TMDO_Entity_Registry::init();
TMDO_Entity_Registry::register_adapter( 'user', $this->createMock( TMDO_Entity_Adapter_Interface::class ) );
}
private function stored_definition(): array {
return array(
array( 'key' => 'orphan_note', 'type' => 'text', 'group' => 'orphan', 'entity_type' => 'user' ),
array( 'key' => 'orphan_score', 'type' => 'integer', 'searchable' => true, 'group' => 'orphan', 'entity_type' => 'user' ),
);
}
public function test_restore_registers_fields_for_routing(): void {
$this->assertTrue( TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() ) );
$this->assertSame( 'orphan', TMDO_Entity_Registry::get_field( 'user', 'orphan_note' )['group'] );
$this->assertSame( array( 'orphan_note', 'orphan_score' ), TMDO_Entity_Registry::get_group_keys( 'user', 'orphan' ) );
}
public function test_restore_never_queues_ddl(): void {
TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() );
$this->assertSame( array(), TMDO_Entity_Registry::get_pending_schemas() );
}
public function test_restore_skips_keys_managed_by_a_live_group(): void {
TMDO_Entity_Registry::register_group( 'user', 'live', array( array( 'key' => 'orphan_note', 'type' => 'text' ) ) );
TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() );
$this->assertSame( 'live', TMDO_Entity_Registry::get_field( 'user', 'orphan_note' )['group'] );
$this->assertSame( array( 'orphan_score' ), TMDO_Entity_Registry::get_group_keys( 'user', 'orphan' ) );
$restored = TMDO_Entity_Registry::get_restored_groups();
$this->assertSame( array( 'orphan_note' ), $restored[0]['skipped'] );
}
public function test_restore_leaves_keys_with_native_data_on_native_meta(): void {
TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition(), array( 'orphan_note' ) );
$this->assertNull( TMDO_Entity_Registry::get_field( 'user', 'orphan_note' ) );
$this->assertSame( array( 'orphan_score' ), TMDO_Entity_Registry::get_group_keys( 'user', 'orphan' ) );
$this->assertSame( array( 'orphan_note' ), TMDO_Entity_Registry::get_restored_groups()[0]['native'] );
}
public function test_group_left_entirely_on_native_is_still_reported(): void {
$this->assertFalse( TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition(), array( 'orphan_note', 'orphan_score' ) ) );
$this->assertNotContains( 'orphan', TMDO_Entity_Registry::get_groups_for_type( 'user' ) );
$restored = TMDO_Entity_Registry::get_restored_groups();
$this->assertSame( 0, $restored[0]['fields'] );
$this->assertSame( array( 'orphan_note', 'orphan_score' ), $restored[0]['native'] );
}
public function test_restore_is_refused_when_group_is_registered(): void {
TMDO_Entity_Registry::register_group( 'user', 'orphan', array( array( 'key' => 'orphan_note', 'type' => 'text' ) ) );
$this->assertFalse( TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() ) );
$this->assertSame( array(), TMDO_Entity_Registry::get_restored_groups() );
}
public function test_restore_is_refused_when_every_key_is_taken(): void {
TMDO_Entity_Registry::register_group(
'user',
'live',
array(
array( 'key' => 'orphan_note', 'type' => 'text' ),
array( 'key' => 'orphan_score', 'type' => 'integer' ),
)
);
$this->assertFalse( TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() ) );
$this->assertNotContains( 'orphan', TMDO_Entity_Registry::get_groups_for_type( 'user' ) );
}
public function test_restored_groups_are_listed_and_cleared_by_init(): void {
TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() );
$restored = TMDO_Entity_Registry::get_restored_groups();
$this->assertSame( 'user', $restored[0]['entity_type'] );
$this->assertSame( 'orphan', $restored[0]['group'] );
$this->assertSame( 2, $restored[0]['fields'] );
TMDO_Entity_Registry::init();
$this->assertSame( array(), TMDO_Entity_Registry::get_restored_groups() );
}
public function test_site_health_reports_restored_groups(): void {
$this->assertSame( 'good', TMDO_Site_Health::check_orphaned_entity_groups()['status'] );
TMDO_Entity_Registry::restore_group( 'user', 'orphan', $this->stored_definition() );
$result = TMDO_Site_Health::check_orphaned_entity_groups();
$this->assertSame( 'recommended', $result['status'] );
$this->assertStringContainsString( 'user/orphan', $result['description'] );
$this->assertArrayHasKey( 'wpdo_orphaned_entity_groups', TMDO_Site_Health::register_tests( array() )['direct'] );
}
}