From 9bda058f4112e09cc50b4dee918fe9f5fdf5da60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 16 May 2026 16:06:59 +0200 Subject: [PATCH] Cover stale term taxonomy semantic references --- docs/merge-reliability.md | 7 ++- tests/cow/wp_semantic_validator.php | 97 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/docs/merge-reliability.md b/docs/merge-reliability.md index d45effab..0b0aa593 100644 --- a/docs/merge-reliability.md +++ b/docs/merge-reliability.md @@ -17,7 +17,7 @@ when there is a test or document that exercises the specific merge invariant. | Objective item | Evidence on trunk | Remaining gap | | --- | --- | --- | -| 1. Real WordPress semantic merge coverage | `tests/cow/e2e.sh` creates source and target branches through runtime WordPress requests, validates each branch-local graph before merge, then merges pages, branch-local page edits/deletes with edited content and authors, postmeta, users/usermeta, authors, comments/commentmeta, hierarchical taxonomy terms, nav menus and menu locations, reusable `wp_block` rows, page-to-reusable-block refs, `core/image` block refs and featured-image refs to media attachments, options and JSON options with branch user/object IDs, media uploads with attachment parents plus generated-size metadata/files, a CPT-like `forkpress_note`, and plugin-shaped custom tables/files. The semantic E2E merge now requires `status: completed` and a zero-conflict merge run, so runtime-only state cannot hide behind a surviving object graph. `tests/cow/merge_smoke.php` now fast-gates the basic "page created on branch plus page created on main both survive merge" invariant without starting WordPress. `tests/cow/wp_semantic_validator.php` is a focused fast gate for discovered WordPress semantic validators that catch pages left pointing at deleted reusable blocks, synced patterns, or navigation blocks, child pages or attachments left pointing at deleted `post_parent` rows, posts or attachments left pointing at deleted `post_author` users, postmeta left pointing at deleted posts, usermeta left pointing at deleted users, nav menu items left pointing at deleted parent menu items, pages, or taxonomy terms, featured-image postmeta left pointing at deleted attachment rows/files, `core/image` block JSON left pointing at deleted attachment rows/files, term relationships left pointing at deleted taxonomy terms, child taxonomy terms left pointing at deleted parent terms, termmeta left pointing at deleted terms, comments left pointing at deleted posts/users/parent comments, commentmeta left pointing at deleted comments, and options/widgets/theme mods left pointing at deleted WordPress objects. `tests/cow/merge.php` adds deterministic WordPress row fingerprint and validator coverage. | Add broader concurrent edit/delete matrices for complete WP objects and deterministic repair policies only where the owner object is unambiguous. | +| 1. Real WordPress semantic merge coverage | `tests/cow/e2e.sh` creates source and target branches through runtime WordPress requests, validates each branch-local graph before merge, then merges pages, branch-local page edits/deletes with edited content and authors, postmeta, users/usermeta, authors, comments/commentmeta, hierarchical taxonomy terms, nav menus and menu locations, reusable `wp_block` rows, page-to-reusable-block refs, `core/image` block refs and featured-image refs to media attachments, options and JSON options with branch user/object IDs, media uploads with attachment parents plus generated-size metadata/files, a CPT-like `forkpress_note`, and plugin-shaped custom tables/files. The semantic E2E merge now requires `status: completed` and a zero-conflict merge run, so runtime-only state cannot hide behind a surviving object graph. `tests/cow/merge_smoke.php` now fast-gates the basic "page created on branch plus page created on main both survive merge" invariant without starting WordPress. `tests/cow/wp_semantic_validator.php` is a focused fast gate for discovered WordPress semantic validators that catch pages left pointing at deleted reusable blocks, synced patterns, or navigation blocks, child pages or attachments left pointing at deleted `post_parent` rows, posts or attachments left pointing at deleted `post_author` users, postmeta left pointing at deleted posts, usermeta left pointing at deleted users, nav menu items left pointing at deleted parent menu items, pages, or taxonomy terms, featured-image postmeta left pointing at deleted attachment rows/files, `core/image` block JSON left pointing at deleted attachment rows/files, term relationships left pointing at deleted taxonomy terms, term taxonomy rows left pointing at deleted terms, child taxonomy terms left pointing at deleted parent terms, termmeta left pointing at deleted terms, comments left pointing at deleted posts/users/parent comments, commentmeta left pointing at deleted comments, and options/widgets/theme mods left pointing at deleted WordPress objects. `tests/cow/merge.php` adds deterministic WordPress row fingerprint and validator coverage. | Add broader concurrent edit/delete matrices for complete WP objects and deterministic repair policies only where the owner object is unambiguous. | | 2. Plugin-specific merge semantics | `docs/plugin-merge-validators.md` defines the validator contract, including rejecting contradictory status/finding output and optional first-class `logical_identity` evidence for plugin-defined object identity. `scripts/cow/merge.php` discovers active plugin and mu-plugin validators, runs explicit validators, records plugin-scoped conflicts, and rolls back inline validator failures. `tests/cow/plugin_validator.php` is a focused fast gate for discovered validator review of plugin-owned DB/JSON/file graphs and serialized/JSON option/postmeta/file graphs, plugin-scoped audit output for incoherent JSON, missing or unsafe file references, stale serialized/JSON asset references, identical validator rerun dedupe, contradictory validator output rejection, replacement-evidence revalidation when validator findings change after review, explicit plugin source-evidence drift recorded by validator reruns, and plugin `logical_identity` drift returning reviewed findings to `needs-action`. `tests/cow/merge.php` covers clean custom-table graph merges, validator findings, audit/review grouping, validator rerun evidence, file-root context, active-plugin discovery, explicit-ID plugin graph validation, contradictory validator output rejection, and failed-validator rollback. `tests/cow/e2e.sh` covers a runtime plugin-shaped graph across custom table parent/child rows, child JSON payload refs, JSON, serialized data, options, postmeta, CPT data, and branch-owned file contents. | Add validators for real plugins and add merge drivers only for plugin-owned repairs that can prove correctness. | | 3. Remaining review-only schema cases | `scripts/cow/merge.php` validates source-added views/triggers/indexes, preserves invalid dependency cases as conflicts, and supports safe schema object resolution for deterministic subsets. `tests/cow/schema_review.php` is a focused fast gate proving acyclic source-added dependent views, views depending on source-added tables, trigger programs, and triggers depending on source-added tables apply in dependency order, cyclic source-added views/triggers stay reviewable, source-added triggers with missing target dependencies stay gated until the dependency is restored, source-added expression unique indexes blocked by target rows stay reviewable until the blocking rows are removed, and reviewed source-added indexes/views/triggers, dropped-table restores, and table rebuild conflicts return to `needs-action` with current source SQL evidence when the reviewed source SQL changes after review. It also covers target-side SQL drift for reviewed source-added view, trigger, index, dropped-table restore, and table rebuild conflicts, including current target SQL evidence and conservative `unclassified` revalidation. `tests/cow/merge.php` covers broader cyclic/invalid view and trigger dependency handling, source-added dependent view/trigger/index ordering, and rebuild validation cases. | Improve dependency planning for more safe reorderings. Add richer schema revalidation evidence for dependency rebuild plans. Cyclic or semantically ambiguous cases should stay review-only. | | 4. Filesystem merge hardening | `tests/cow/filesystem.php` is a focused fast gate for safe source text/binary file application, conflicting binary file edits staying target-kept with hash payload metadata instead of text decoding, safe relative symlink changes/additions, unsafe absolute/root-escaping/self-referential/managed-path symlinks staying as auditable file conflicts, directory/file type replacements staying review-held until an explicit audited source resolution applies them, and source directory deletions staying review-held when target descendants exist. `tests/cow/media_validator.php` fast-gates discovered upload validators for missing required attachment metadata rows, invalid serialized attachment metadata, invalid original/generated dimensions, original/generated filesize drift, malformed or incomplete generated-size metadata, generated-size filename drift, missing original/generated upload files from both `_wp_attached_file` and `_wp_attachment_metadata['file']`, duplicate original/generated upload ownership, unsafe primary/metadata/generated upload paths, and `_wp_attached_file` versus `_wp_attachment_metadata['file']` drift. `tests/cow/merge.php` covers file adds/deletes/conflicts, binary hash comparisons, symlink safety, directory/file and file/directory replacement review, rollback artifacts, upload-file validators, generated attachment file checks, original/generated dimension drift, generated-size filename drift, featured-image/image-block/media metadata drift, and unsafe metadata paths. `tests/cow/e2e.sh` verifies real merged upload originals and generated thumbnails. | Add stricter uploads-specific validators for more drift shapes and explicit attachment-regeneration decisions. | @@ -48,7 +48,7 @@ when there is a test or document that exercises the specific merge invariant. | Area | Current state | Missing reliability work | | --- | --- | --- | -| WordPress semantic objects | Tests cover real post creation, postmeta references, users, usermeta, post/comment authors, threaded comments and commentmeta references, branch-local page edits/deletes with edited content/author assertions, same-object page/postmeta edit-vs-delete conflicts with auditable target-wins defaults, attachment uploads plus original and generated-size files, attachment metadata, attachment-to-page parent links, `core/image` block references and featured-image postmeta references to media attachments, hierarchical taxonomy terms, page-linked nav menus with menu-location assignments, reusable blocks and synced patterns, options with embedded object IDs including branch user refs, JSON option payloads with embedded object IDs including branch user refs, custom post types, plugin AUTOINCREMENT tables, keyless plugin tables, unique collisions, file additions, nested plugin-owned custom-table/JSON/serialized/file graphs, branch merge visibility, a clean zero-conflict semantic E2E merge requirement, a discovered media validator that reports missing original/generated upload files including metadata-side original files, duplicate attachment claims on the same upload file including same-attachment generated-file duplicates, unreadable or NUL-corrupted attachment metadata, empty or unsafe primary/metadata/generated upload metadata paths, original/generated dimension drift, malformed or incomplete generated-size metadata, generated-size filename drift, and `_wp_attached_file` versus `_wp_attachment_metadata` file drift, fast discovered block-reference, post-parent-reference, post-author-reference, postmeta-reference, usermeta-reference, menu-parent-reference, menu-reference, featured-image, image-block, term-relationship, term-parent-reference, termmeta-reference, and option-reference validators that report pages/posts left pointing at deleted reusable blocks, synced patterns, or navigation blocks, child pages or attachments left pointing at deleted `post_parent` rows, posts or attachments left pointing at deleted `post_author` users, postmeta rows left pointing at deleted posts, usermeta rows left pointing at deleted users, nav menu items left pointing at deleted parent menu items or deleted post/taxonomy objects, `_thumbnail_id` postmeta left pointing at deleted attachment objects/files, `core/image` block JSON left pointing at deleted attachment objects/files, `wp_term_relationships` left pointing at deleted taxonomy term rows, child taxonomy terms left pointing at deleted parent terms, termmeta rows left pointing at deleted terms, serialized theme mods left pointing at deleted post objects, deleted nav-menu terms, or deleted custom-logo attachments plus serialized nav menu widgets, serialized media-image widgets, serialized sidebar-widget placements, scalar `site_icon`/`page_on_front`/`page_for_posts` options, and serialized `sticky_posts` options left pointing at deleted objects, and `docs/merge-repair-policy.md` defines when semantic repairs must remain review-only. | Add broader concurrent object matrices, implement only the repair policies that have deterministic owners, and broaden plugin-owned graph conflict/drift cases. | +| WordPress semantic objects | Tests cover real post creation, postmeta references, users, usermeta, post/comment authors, threaded comments and commentmeta references, branch-local page edits/deletes with edited content/author assertions, same-object page/postmeta edit-vs-delete conflicts with auditable target-wins defaults, attachment uploads plus original and generated-size files, attachment metadata, attachment-to-page parent links, `core/image` block references and featured-image postmeta references to media attachments, hierarchical taxonomy terms, page-linked nav menus with menu-location assignments, reusable blocks and synced patterns, options with embedded object IDs including branch user refs, JSON option payloads with embedded object IDs including branch user refs, custom post types, plugin AUTOINCREMENT tables, keyless plugin tables, unique collisions, file additions, nested plugin-owned custom-table/JSON/serialized/file graphs, branch merge visibility, a clean zero-conflict semantic E2E merge requirement, a discovered media validator that reports missing original/generated upload files including metadata-side original files, duplicate attachment claims on the same upload file including same-attachment generated-file duplicates, unreadable or NUL-corrupted attachment metadata, empty or unsafe primary/metadata/generated upload metadata paths, original/generated dimension drift, malformed or incomplete generated-size metadata, generated-size filename drift, and `_wp_attached_file` versus `_wp_attachment_metadata` file drift, fast discovered block-reference, post-parent-reference, post-author-reference, postmeta-reference, usermeta-reference, menu-parent-reference, menu-reference, featured-image, image-block, term-relationship, term-taxonomy-reference, term-parent-reference, termmeta-reference, and option-reference validators that report pages/posts left pointing at deleted reusable blocks, synced patterns, or navigation blocks, child pages or attachments left pointing at deleted `post_parent` rows, posts or attachments left pointing at deleted `post_author` users, postmeta rows left pointing at deleted posts, usermeta rows left pointing at deleted users, nav menu items left pointing at deleted parent menu items or deleted post/taxonomy objects, `_thumbnail_id` postmeta left pointing at deleted attachment objects/files, `core/image` block JSON left pointing at deleted attachment objects/files, `wp_term_relationships` left pointing at deleted taxonomy term rows, term taxonomy rows left pointing at deleted terms, child taxonomy terms left pointing at deleted parent terms, termmeta rows left pointing at deleted terms, serialized theme mods left pointing at deleted post objects, deleted nav-menu terms, or deleted custom-logo attachments plus serialized nav menu widgets, serialized media-image widgets, serialized sidebar-widget placements, scalar `site_icon`/`page_on_front`/`page_for_posts` options, and serialized `sticky_posts` options left pointing at deleted objects, and `docs/merge-repair-policy.md` defines when semantic repairs must remain review-only. | Add broader concurrent object matrices, implement only the repair policies that have deterministic owners, and broaden plugin-owned graph conflict/drift cases. | | Plugin-specific semantics | Generic SQLite merge is table/row/cell based and does not rewrite embedded IDs. `docs/plugin-merge-validators.md` defines the validator boundary and first test shape. PHP unit and E2E coverage now cover the clean happy path for a plugin-owned custom-table graph with parent/child rows, child JSON payload references, serialized option/postmeta references, referenced CPT data, and a referenced file. The PHP unit suite also covers the metadata/audit foundation for plugin-scoped validator conflicts, including review queues and grouping. Normal branch merges discover validators from active plugin and mu-plugin locations in the staged candidate target; discovered custom-table graph validators can abort and roll back a candidate with a broken JSON reference, or complete the merge with plugin-scoped review conflicts for broken serialized graph row/file references and target-conflicting graph state. The focused plugin validator gate now includes serialized and JSON plugin option/postmeta references left pointing at a deleted plugin asset row/file or an unsafe plugin-owned file path, plus first-class plugin `logical_identity` drift evidence for semantic identities that are not expressed as SQLite keys. `forkpress branch run-plugin-validator`, `forkpress branch record-plugin-validator-conflicts`, and `forkpress branch merge --plugin-validator ` expose explicit validator execution and findings recording, while rejecting contradictory valid-with-findings output before it becomes conflict metadata. Validator failures after DB/files have staged roll back the merge. | Add broader plugin-owned validators for more real plugins and plugin merge drivers only where a plugin can prove an automatic repair is safe. | | Review-only schema cases | Acyclic source-added dependent views, views that depend on source-added tables, trigger programs, and triggers that depend on source-added tables can apply automatically in dependency order. Source-added expression unique indexes that target rows would violate stay reviewable until those rows are removed. Reviewed source-added index/view/trigger conflicts, dropped-table restores, and table rebuild conflicts can be revalidated for changed source/target SQL evidence and return to `needs-action` without claiming compatibility. Cyclic views/triggers, source-added triggers with unresolved dependencies, invalid preserved trigger/view dependencies, and some rebuild dependency chains are held as auditable conflicts. | Improve dependency planning so more safe schema reorderings can apply automatically. Add richer schema revalidation evidence for dependency rebuild plans. Keep non-deterministic or semantically ambiguous cases review-only. | | Filesystem semantics | File additions/deletions/conflicts are audited; binary file changes/conflicts are hash-verified, safe relative symlinks can merge, unsafe symlinks to absolute paths, root-escaping paths, self-references, and ForkPress-managed paths remain conflicts, directory/file and file/directory replacements get type-specific review conflicts, unchanged target descendants and source descendants under reviewed replacements are held until review, source directory deletions with target-side descendants are held until review, reviewed source replacements can apply supported file/dir/symlink changes including directory subtrees, WordPress E2E links attachment rows to original and generated-size upload files, plugin-shaped E2E checks branch-owned file contents, and PHP coverage uses a discovered validator to cross-check attachment metadata against merged upload files, missing required attachment metadata rows, invalid serialized attachment metadata, invalid original/generated dimensions, original/generated filesize drift, generated-size filename drift, missing original/generated upload files including metadata-side original files, attached-file metadata drift, duplicate original/generated upload ownership, and root-escaping primary/metadata/generated upload paths. | Add stricter uploads-specific validators for more conflict/drift shapes, including attachment metadata regeneration decisions. | @@ -173,7 +173,8 @@ usermeta rows left pointing at deleted users, and nav menu items left pointing at deleted parent menu items or deleted pages, plus featured-image postmeta left pointing at deleted attachments/files and `core/image` block JSON left pointing at deleted attachments/files, term relationships left pointing at deleted taxonomy terms, -child taxonomy terms left pointing at deleted parent terms, termmeta rows left +term taxonomy rows left pointing at deleted terms, child taxonomy terms left +pointing at deleted parent terms, termmeta rows left pointing at deleted terms, comments left pointing at deleted posts/users/parent comments, commentmeta left pointing at deleted comments, and options/widgets/theme mods left pointing diff --git a/tests/cow/wp_semantic_validator.php b/tests/cow/wp_semantic_validator.php index 99b4f000..fb0da23d 100644 --- a/tests/cow/wp_semantic_validator.php +++ b/tests/cow/wp_semantic_validator.php @@ -289,6 +289,17 @@ function create_wp_term_relationship_db(string $path): void { $db->close(); } +function create_wp_term_taxonomy_reference_db(string $path): void { + $db = open_db($path); + $db->exec('CREATE TABLE wp_terms (term_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, slug TEXT NOT NULL, term_group INTEGER NOT NULL DEFAULT 0)'); + $db->exec('CREATE TABLE wp_term_taxonomy (term_taxonomy_id INTEGER PRIMARY KEY AUTOINCREMENT, term_id INTEGER NOT NULL, taxonomy TEXT NOT NULL, description TEXT NOT NULL DEFAULT "", parent INTEGER NOT NULL DEFAULT 0, count INTEGER NOT NULL DEFAULT 0)'); + $db->exec("INSERT INTO wp_terms (term_id, name, slug) VALUES (89, 'Deleted taxonomy owner', 'deleted-taxonomy-owner')"); + $db->exec("INSERT INTO wp_term_taxonomy (term_taxonomy_id, term_id, taxonomy, description, count) VALUES + (90, 89, 'category', 'Base category taxonomy', 0), + (91, 89, 'post_tag', 'Base tag taxonomy', 0)"); + $db->close(); +} + function create_wp_term_parent_reference_db(string $path): void { $db = open_db($path); $db->exec('CREATE TABLE wp_terms (term_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, slug TEXT NOT NULL, term_group INTEGER NOT NULL DEFAULT 0)'); @@ -1391,6 +1402,92 @@ function create_wp_option_reference_db(string $path): void { $term_preview = (string)($term_audit['conflicts'][0]['chosen_preview'] ?? ''); assert_true(str_contains($term_preview, '"term_taxonomy_id":82'), 'WordPress term relationship audit includes the missing term taxonomy ID'); + $term_taxonomy_base_root = $tmp . '/term-taxonomy-base'; + $term_taxonomy_source_root = $tmp . '/term-taxonomy-source'; + $term_taxonomy_target_root = $tmp . '/term-taxonomy-target'; + $term_taxonomy_base = $term_taxonomy_base_root . '/wp-content/database/.ht.sqlite'; + $term_taxonomy_source = $term_taxonomy_source_root . '/wp-content/database/.ht.sqlite'; + $term_taxonomy_target = $term_taxonomy_target_root . '/wp-content/database/.ht.sqlite'; + $term_taxonomy_metadata = $tmp . '/.forkpress/cow/merge/wp-term-taxonomy-validator-metadata.sqlite'; + $term_taxonomy_file_base = $tmp . '/.forkpress/cow/merge/file-bases/wp-term-taxonomy-validator.json'; + + mkdir($term_taxonomy_base_root . '/wp-content/database', 0777, true); + create_wp_term_taxonomy_reference_db($term_taxonomy_base); + write_test_file($term_taxonomy_base_root . '/wp-content/mu-plugins/forkpress-merge-validator.php', <<<'PHP' +query("SELECT term_taxonomy_id, term_id, taxonomy FROM wp_term_taxonomy WHERE term_id > 0"); +$findings = []; +while ($row = $res->fetchArray(SQLITE3_ASSOC)) { + $term_id = (int)$row['term_id']; + $exists = (int)$db->querySingle("SELECT COUNT(*) FROM wp_terms WHERE term_id = $term_id"); + if ($exists === 0) { + $findings[] = [ + 'plugin' => 'forkpress-wp-term-taxonomy-refs', + 'object' => 'term_taxonomy:' . $row['term_taxonomy_id'], + 'reason' => 'term taxonomy references a missing term', + 'type' => 'plugin-wp-term-taxonomy-missing-term', + 'tables' => ['wp_term_taxonomy', 'wp_terms'], + 'validator' => 'forkpress-wp-term-taxonomy-refs@1', + 'candidate' => [ + 'term_taxonomy_id' => (int)$row['term_taxonomy_id'], + 'taxonomy' => (string)$row['taxonomy'], + 'field' => 'term_id', + 'missing_term_id' => $term_id, + ], + ]; + } +} +echo json_encode([ + 'status' => $findings ? 'conflicts' : 'valid', + 'findings' => $findings, +], JSON_UNESCAPED_SLASHES); +PHP); + copy_tree_for_test($term_taxonomy_base_root, $term_taxonomy_source_root); + copy_tree_for_test($term_taxonomy_base_root, $term_taxonomy_target_root); + cow_merge_capture_file_base($term_taxonomy_base_root, $term_taxonomy_file_base); + cow_merge_allocate_autoincrement_bands($term_taxonomy_source, $term_taxonomy_metadata, 'feature-wp-term-taxonomy-source'); + cow_merge_allocate_autoincrement_bands($term_taxonomy_target, $term_taxonomy_metadata, 'main'); + + $db = open_db($term_taxonomy_source); + $db->exec('DELETE FROM wp_terms WHERE term_id = 89'); + $db->close(); + + $db = open_db($term_taxonomy_target); + $db->exec("UPDATE wp_term_taxonomy SET description = 'Target category taxonomy still pointing at deleted term' WHERE term_taxonomy_id = 90"); + $db->exec("UPDATE wp_term_taxonomy SET description = 'Target tag taxonomy still pointing at deleted term' WHERE term_taxonomy_id = 91"); + $db->close(); + + $term_taxonomy_result = cow_merge_branch_state( + $term_taxonomy_base, + $term_taxonomy_source, + $term_taxonomy_target, + $term_taxonomy_metadata, + 'feature-wp-term-taxonomy-source', + 'main', + $term_taxonomy_file_base, + $term_taxonomy_source_root, + $term_taxonomy_target_root + ); + + assert_same($term_taxonomy_result['status'], 'completed_with_conflicts', 'WordPress term-taxonomy validator holds missing terms for review'); + assert_same((int)($term_taxonomy_result['plugin_validators'] ?? 0), 1, 'WordPress term-taxonomy validator is discovered from mu-plugins during merge'); + assert_same((int)($term_taxonomy_result['plugin_validator_conflicts'] ?? 0), 2, 'WordPress term-taxonomy validator records missing term owners for multiple taxonomies'); + assert_same((int)scalar($term_taxonomy_target, 'SELECT COUNT(*) FROM wp_terms WHERE term_id = 89'), 0, 'WordPress term-taxonomy validator leaves the source term deletion staged for review'); + assert_same(scalar($term_taxonomy_target, 'SELECT description FROM wp_term_taxonomy WHERE term_taxonomy_id = 90'), 'Target category taxonomy still pointing at deleted term', 'WordPress term-taxonomy validator preserves the target category taxonomy edit'); + assert_same(scalar($term_taxonomy_target, 'SELECT description FROM wp_term_taxonomy WHERE term_taxonomy_id = 91'), 'Target tag taxonomy still pointing at deleted term', 'WordPress term-taxonomy validator preserves the target tag taxonomy edit'); + + $term_taxonomy_audit = cow_merge_audit_report($term_taxonomy_metadata, (int)$term_taxonomy_result['run_id'], 10, [ + 'scope' => 'plugin', + 'records' => 'conflicts', + 'conflict_type' => 'plugin-wp-term-taxonomy-missing-term', + ]); + assert_same(count($term_taxonomy_audit['conflicts']), 2, 'WordPress term-taxonomy validator exposes missing terms as plugin-scoped audit conflicts'); + $term_taxonomy_preview = implode("\n", array_map(fn($conflict) => (string)($conflict['chosen_preview'] ?? ''), $term_taxonomy_audit['conflicts'])); + assert_true(str_contains($term_taxonomy_preview, '"missing_term_id":89'), 'WordPress term-taxonomy audit includes the missing term ID'); + assert_true(str_contains($term_taxonomy_preview, '"field":"term_id"'), 'WordPress term-taxonomy audit includes the stale field name'); + assert_true(str_contains($term_taxonomy_preview, '"taxonomy":"post_tag"'), 'WordPress term-taxonomy audit includes the affected taxonomy'); + $term_parent_base_root = $tmp . '/term-parent-base'; $term_parent_source_root = $tmp . '/term-parent-source'; $term_parent_target_root = $tmp . '/term-parent-target';