diff --git a/docs/merge-reliability.md b/docs/merge-reliability.md index c9389446..b3c17513 100644 --- a/docs/merge-reliability.md +++ b/docs/merge-reliability.md @@ -182,7 +182,7 @@ Recent additions in the current merge-reliability work: | 6. Branch birth always captures merge bases | `crates/forkpress-storage/src/lib.rs` requires branch birth metadata for branch reuse/merge and blocks pending reset states. `tests/cow/branch_birth.php` fast-gates required ID bands, keyless row identities, filesystem merge-base capture as a frozen pre-write snapshot with managed DB/config/Git exclusions, cleanup of rollback metadata, and cleanup isolation for unrelated branch metadata. `tests/cow/git_server.php` covers Git-created branch DB/file base, ID-band, row identity, decision/run metadata, branch-birth decision cleanup, DB merge-base sidecar cleanup, file-base cleanup, and cleanup/rollback paths. `tests/cow/e2e.sh` covers public create retry after interrupted birth metadata, public reset retry after interrupted reset publication, and remote-cache branch creation followed by AUTOINCREMENT-band insertion and mergeback to `main`. | Keep every new creation/reuse/reset path under the same invariant and add regressions whenever a new branch publication path is introduced. | | 7. ID-band enforcement beyond happy paths | `tests/cow/id_bands.php` is a focused fast gate for separate branch AUTOINCREMENT bands, JSON/serialized references that keep branch IDs distinct without rewrite, normal in-band branch reuse that refreshes existing bands instead of allocating fresh ones, reset protection that allocates fresh bands when a branch DB drops below its old reservation, non-colliding non-AUTOINCREMENT `INTEGER PRIMARY KEY` plugin rows, review-held non-AUTOINCREMENT `INTEGER PRIMARY KEY` plugin collisions including ordinary implicit rowid allocation where both branches independently receive `id=1`, and audit/text output that names non-bandable plugin tables and explains the missing durable `sqlite_sequence` reservation point. `tests/cow/explicit_ids.php` fast-gates in-band explicit AUTOINCREMENT imports that should merge without ID rewrite, fresh band allocation when an in-band explicit import reaches the previous band end before the next implicit insert, out-of-band AUTOINCREMENT inserts and primary-key rewrites for WordPress and plugin tables, paired source deletes held behind explicit inserts, inserted or updated `wp_posts` rows with `post_author`, `core/avatar`, and `core/query` author references behind out-of-band explicit `wp_users` imports, inserted or updated `wp_usermeta` and `wp_comments` rows held behind out-of-band explicit `wp_users` imports, inserted or updated `wp_commentmeta` plus threaded comments behind out-of-band explicit `wp_comments` imports, inserted or updated featured-image postmeta, image-block content, classic `wp-image-*` and `[gallery ids="..."]` content, `site_icon`, theme-mod `custom_logo`, and media/block/text/custom HTML widget options held behind out-of-band explicit attachment imports, pages-widget options and inserted or updated `core/navigation-link`, `core/navigation-submenu`, and `core/navigation` post_content refs behind out-of-band explicit page/term/`wp_navigation` imports, and inserted or updated `wp_termmeta`, `wp_term_taxonomy`, `wp_term_relationships`, serialized theme-mod menu locations, nav-menu widgets, and `nav_menu_options` options held behind out-of-band explicit `wp_terms` imports. `tests/cow/merge.php` covers AUTOINCREMENT allocation, rollback, reset below old bands, independent branch IDs, explicit out-of-band source IDs, child rows behind held explicit post/term/user IDs, inserted and updated scalar/serialized/theme/widget `wp_options`, `wp_posts`, `wp_postmeta`, `wp_comments`, `wp_commentmeta`, `wp_usermeta`, `wp_termmeta`, `wp_term_taxonomy`, `wp_term_relationships`, post-author, taxonomy menu-item, reusable/media/avatar/navigation/query block `post_content`, and comment-user references behind held explicit post/term/user IDs, JSON/serialized references that keep branch IDs distinct, plugin validator review for no-FK child rows behind held explicit plugin AUTOINCREMENT parents, and non-AUTOINCREMENT `INTEGER PRIMARY KEY` plugin graph collisions as review-held. `tests/cow/e2e.sh` verifies runtime branch post IDs fall inside branch bands and requires an independently banded source/target WordPress post merge to finish with `status: completed` and zero recorded conflicts while preserving embedded JSON/serialized post IDs. | Expand explicit-ID/import handling beyond currently covered AUTOINCREMENT row-insert/rewrite cases and enforce review for more plugin/custom logical identities that are not safely bandable. | | 8. Better stale-audit workflow | `docs/stale-audit-workflow.md` describes the revalidation model. `scripts/cow/merge.php` implements `revalidate-reviews`, `merge-audit --revalidate`, `merge-resolve --after-revalidate`, revalidation classes, latest revalidation status checks, source/target drift checks, schema index/view/trigger/table-restore/table-rebuild source/target drift checks, plugin validator replacement evidence, plugin replacement conflict links, WordPress semantic fingerprints, conservative non-PK `UNIQUE` logical-key drift detection for custom/plugin tables, source/target row-context payloads for new database cell conflicts, current `--resolution-choice` and `--blocked-resolution-choice` conflict queue filters, `--event-type`, conflict-event grouping, blocked resolution attempt events, resolver-contract filtering/grouping by resolution strategy, generic resolver support, and after-revalidate support, `--latest-revalidation-status`, `--stale-status`, and matching group-by queue filters, and text/JSON revalidation summaries that name the carried and already-open `needs-action` conflict ids, classifiers, drift reasons, revalidation records, and replacement conflict ids. The CLI also exposes `forkpress branch conflicts` as a shortcut for `merge-audit --records conflicts`, with the same filters and revalidation contract, so conflict review queues do not require remembering the lower-level audit record selector. `tests/cow/stale_audit.php` is a focused fast gate for reviewed cell conflicts, target drift detection, source cell/row drift detection, deleted target rows classified as `missing`, no-primary-key rowid replacement classified as `incompatible`, WordPress post, postmeta, option, term, term taxonomy, termmeta, user, usermeta, comment, and commentmeta semantic replacements classified as `incompatible`, custom/plugin `UNIQUE` logical-key replacement on source or target classified as `incompatible`, row-context-backed source/target logical-key replacement for cell conflicts where the reviewed cell value itself is unchanged, `needs-action` carry-forward, idempotent revalidation, actionable revalidation summaries, audit-visible revalidation classes, latest revalidation current/drifted status output, live stale-status filtering/grouping, and guarded `--after-revalidate` source resolution. `tests/cow/plugin_validator.php` fast-gates plugin validator replacement evidence when rerun findings change, including explicit changed plugin `source` payload evidence and changed first-class plugin `logical_identity` evidence. `tests/cow/schema_review.php` fast-gates source-added schema index/view/trigger, dropped-table restore, and table rebuild source drift returning reviewed schema conflicts to `needs-action` with current source SQL evidence; source-added view/trigger source drift can get compatible classes and guarded `--after-revalidate` source resolution when current source SQL now validates, while source-added index, view, and trigger target drift get compatible classes and guarded source resolution only when dry-run source replacement validates. Table rebuild payloads include direct indexes/triggers, dependent views, and dependent view triggers, so dependency-only source drift is visible even when table SQL is unchanged. It also covers target-side source-added view, trigger, index, dropped-table restore, and table rebuild drift with current target SQL evidence. `tests/cow/merge.php` covers stale row/cell/file drift, source drift, deleted targets, no-PK rowid replacement, supported WordPress semantic fingerprints, guarded resolution, live source-applicable and source-blocked audit filters, conflict event-type filtering and grouping, blocked resolution attempt events, resolver-contract filtering/grouping, idempotent carried notes, plugin validator rerun evidence through direct and `merge-audit --revalidate` paths, duplicate identical validator rerun handling, and replacement validator conflict ids. | Add guarded plugin/schema-specific resolution flows beyond currently validated schema drift classes where a plugin or schema planner can prove compatibility. | -| 9. Release gate issue | `scripts/build-dist.sh` and release preflight tests fail earlier when static-PHP prerequisites are missing, avoid macOS bash empty-array expansion under `set -u` while wrapping Apple Silicon `spc` commands in `arch -arm64`, and `docs/merge-reliability.md` tracks aarch64 macOS as a release gate. Release `v0.1.40` was published from workflow run `26050697871` at release commit `414c9daeb9500adea0ae95dcf36cebcb9e294d6e` after all five release targets built, packaged, and smoke-tested: `aarch64-apple-darwin`, `x86_64-apple-darwin`, `aarch64-unknown-linux-musl`, `x86_64-unknown-linux-musl`, and `x86_64-pc-windows-msvc`. The Apple Silicon archive is available at `https://github.com/Automattic/forkpress/releases/download/v0.1.40/forkpress-aarch64-apple-darwin.tar.gz`, uploaded with the other release assets, `ForkPressSetup.exe`, and `SHA256SUMS`. The mac APFS e2e gate still tolerates only the known transient `hdiutil compact` "Resource temporarily unavailable" failure after storage has already detached. | Keep aarch64 macOS release and APFS sparsebundle E2E green on trunk for future releases; do not treat a transient compact skip as proof that compaction itself succeeded. | +| 9. Release gate issue | `scripts/build-dist.sh` and release preflight tests fail earlier when static-PHP prerequisites are missing, avoid macOS bash empty-array expansion under `set -u` while wrapping Apple Silicon `spc` commands in `arch -arm64`, and `docs/merge-reliability.md` tracks aarch64 macOS as a release gate. Release `v0.1.41` was published from workflow run `26051644782` at release commit `8ae3a784a284dc2e9e7475318709c1edb58b6f96` after all five release targets built, packaged, and smoke-tested: `aarch64-apple-darwin`, `x86_64-apple-darwin`, `aarch64-unknown-linux-musl`, `x86_64-unknown-linux-musl`, and `x86_64-pc-windows-msvc`. The Apple Silicon archive is available at `https://github.com/Automattic/forkpress/releases/download/v0.1.41/forkpress-aarch64-apple-darwin.tar.gz`, uploaded with the other release assets, `ForkPressSetup.exe`, and `SHA256SUMS`. The mac APFS e2e gate still tolerates only the known transient `hdiutil compact` "Resource temporarily unavailable" failure after storage has already detached. | Keep aarch64 macOS release and APFS sparsebundle E2E green on trunk for future releases; do not treat a transient compact skip as proof that compaction itself succeeded. | ## Current Guarantees @@ -244,7 +244,7 @@ Recent focused coverage also tightens three roadmap edges: | Branch birth | CLI, Git-created, and remote-cache branches allocate ID bands and capture merge base metadata. Branch-birth validation rejects missing ID bands and keyless row identities, filesystem base capture records user content/uploads while excluding managed DB/config/Git files, and rollback cleanup removes only the failed branch metadata and preserves unrelated branch birth metadata. Git-created branches finalize birth metadata before publishing the branch tree, so a pre-metadata crash cannot expose a branch without ID bands or row identities. Existing branches reused by automation or updated through Git publication must still have a database, DB merge base, filesystem merge base, and required birth metadata before reuse/update. The WordPress admin branch create/merge UI is covered as a thin wrapper over the same CLI paths, including validation, CLI failure surfacing, and real runtime E2E create/merge requests. Remote-cache branch E2E coverage registers a materialized `main` cache, creates `remote-cache-branch`, verifies branch storage, inserts into the runtime AUTOINCREMENT probe inside the branch band, then merges branch DB and filesystem changes back to `main`. | Keep branch create, Git ref create, remote-cache branch, reset, UI creation, and Git-updated existing branches on one invariant: DB base, file base, ID bands, and metadata must exist before user writes. | | ID bands | AUTOINCREMENT bands protect common WordPress and plugin tables. Normal branch reuse inside the reserved band refreshes existing metadata instead of allocating fresh IDs; reaching the previous band end allocates a fresh band before the next implicit insert while preserving prior reserved ranges as valid for merge; reset below old bands gets fresh bands. Existing non-main Git branch updates with a WordPress SQLite database now validate DB merge base, filesystem merge base, and branch-birth metadata before replacing branch storage, so pushes cannot write into an ID-bearing branch that is missing its band reservation. Explicit source IDs outside the reserved branch band are review-held instead of applied automatically for core WordPress and plugin AUTOINCREMENT tables, paired source deletes in the same AUTOINCREMENT table are also held when an out-of-band explicit insert or primary-key rewrite is held, and source child `wp_posts`, owner/reference `wp_postmeta` including inserted and updated type-aware nav menu object refs, inserted or updated scalar/serialized/theme/widget `wp_options` references, inserted or updated `wp_comments`, `wp_commentmeta`, inserted or updated `wp_termmeta`, hierarchical and updated `wp_term_taxonomy`, inserted or primary-key-rewritten `wp_term_relationships`, inserted or updated `wp_usermeta`, inserted or updated post authors, inserted or updated reusable/media/avatar/latest-posts/navigation/query block `post_content` refs including `taxQuery`, inserted or updated classic attachment refs in `post_content`, inserted or updated taxonomy menu-item object refs, or inserted or updated comment user refs pointing at held explicit post/term/user IDs are review-held instead of leaving orphan WordPress child rows. Focused explicit-ID coverage now includes inserted or updated `wp_posts` rows with `post_author`, `core/avatar`, and `core/query` author references plus `wp_usermeta` and `wp_comments` rows behind held out-of-band explicit `wp_users` imports, inserted or updated `wp_commentmeta` and threaded comments behind held explicit `wp_comments` imports, inserted or updated featured-image postmeta, image-block content, classic `wp-image-*` and `[gallery ids="..."]` content, `site_icon`, theme-mod `custom_logo`, and media/block/text/custom HTML widget options behind held explicit attachment imports, pages-widget options and inserted or updated `core/navigation-link`, `core/navigation-submenu`, and `core/navigation` post_content refs behind held explicit page/term/`wp_navigation` imports, and inserted or updated `wp_termmeta`, `wp_term_taxonomy`, `wp_term_relationships`, serialized theme-mod menu locations, nav-menu widgets, and `nav_menu_options` options behind held explicit `wp_terms` imports. Non-AUTOINCREMENT `INTEGER PRIMARY KEY` plugin tables are marked non-bandable; non-colliding rows merge, while collisions are review-held and auditable. Dependent source child rows are also held when their foreign key points at a source parent row whose plain integer key collides with a different target parent. | Expand explicit-ID/import handling beyond covered AUTOINCREMENT row-insert cases and reject/review unsafe reuse for more plugin/custom logical identities that are not safely bandable. | | Stale audits | Resolution fails if target no longer matches the audited payload. `forkpress branch revalidate-reviews` and `forkpress branch merge-audit --revalidate` carry stale reviewed conflicts back into `needs-action` while preserving prior reviewer intent, avoiding duplicate carried notes, recording revalidated payloads, linking plugin replacement validator conflicts, emitting actionable text/JSON summaries of carried and already-open conflict ids, and storing a conservative revalidation classifier such as `compatible-target-drift`, `compatible-source-drift`, `missing`, no-primary-key rowid-reuse `incompatible`, supported WordPress post/postmeta/option/term/term-taxonomy/termmeta/user/usermeta/comment/commentmeta primary-key semantic `incompatible`, custom/plugin non-PK `UNIQUE` logical-key `incompatible`, plugin `replacement-evidence`, schema `compatible-schema-index-target-drift`, `compatible-schema-view-target-drift`, `compatible-schema-trigger-target-drift`, `compatible-schema-table-target-drift`, or schema index/view/trigger/table-restore/table-rebuild `unclassified` drift. A latest `revalidation-required` event now overrides older unapplied validated choices, and bulk `apply-reviewed-resolutions` preflights live staleness, revalidates stale validated choices, reports them as skipped, and leaves the target unchanged instead of surfacing a raw resolver error or silently applying a stale reviewed choice. If a later revalidation shows the source and target payloads are fresh again, the prior `pending` or `reviewed` intent is restored and validated choices re-enter the `apply-reviewed` queue. New database cell conflicts also retain source/target row-context payloads, so custom/plugin logical-key replacement can be detected even when the reviewed scalar cell value is unchanged. `forkpress branch merge-audit --resolution-choice source` and `--blocked-resolution-choice source` expose the current executable/blocking contract for conflict queues. `forkpress branch merge-audit --event-type recorded|review-pending|review-needs-action|review-reviewed|resolution-validated|resolution-applied|resolution-blocked|revalidation-required` exposes the first-class conflict event history without scraping review notes, including failed resolver attempts that leave conflicts open, and `forkpress branch merge-audit --records conflict-events --group-by event-type|lifecycle|conflict-key` summarizes that history for UI queues. `forkpress branch merge-audit --latest-revalidation-status current|source-drifted|target-drifted|source-and-target-drifted|unknown|none` and `--group-by latest-revalidation-status` expose whether the latest recorded after-revalidate guard still matches live source/target state before the reviewer attempts a guarded resolution. `forkpress branch merge-resolve conflict --after-revalidate` can apply reviewed stale database row/cell and filesystem conflicts only when the current source/target payloads still match the latest revalidation record, the latest classifier is not `incompatible`, and the original logical row still exists; source-added view/trigger compatible source drift, source-added schema index/view/trigger compatible target drift, compatible table-rebuild source drift, and compatible table-rebuild target drift can also source-apply after revalidation only when the planner recorded its matching compatible schema class. The fast stale-audit gate now covers deleted target rows, no-primary-key rowid reuse, supported WordPress semantic replacement classifiers, custom/plugin logical-key replacements inferred from non-PK `UNIQUE` indexes, row-context-backed cell conflict identity drift, stale validated choices being excluded or preflight-revalidated by batch apply, fresh-again restoration into the batch apply queue, live source-applicable/source-blocked audit filters, event-type output/filtering/grouping, latest revalidation status output/filtering/grouping, and the direct revalidation output shape. Plugin validator conflicts now return to `needs-action` when a validator rerun records changed evidence for the same plugin object, including explicit changed plugin `source` payload evidence or changed first-class `logical_identity` evidence. Generic source/target resolution remains blocked for plugin conflicts, but a plugin driver can resolve the current replacement conflict when the previous reviewed conflict has a latest current `replacement-evidence` revalidation pointing at that replacement; stale originals, unrevalidated replacements, incompatible revalidations, and drifted replacement evidence are rejected. Ordinary cross-run plugin conflict lineage remains outside that replacement-evidence guard. Reviewed schema index/view/trigger/table-restore/table-rebuild conflicts now return to `needs-action` with current SQL and, for table rebuilds, dependency-plan evidence when schema SQL or dependent object SQL drifts. Other schema conflicts still need planner-backed guarded resolution before they can be applied after revalidation. `docs/stale-audit-workflow.md` maps the current flow and the remaining richer classifier model. | Add more guarded revalidation resolution for plugin and schema conflicts where a plugin or schema planner can prove compatibility. | -| Release gates | Linux, Windows, x86_64 macOS, and aarch64 macOS production bundle artifacts built in the last checked release. macOS and Linux release workflows install static PHP build prerequisites up front, `scripts/build-dist.sh` now fails before cloning/building PHP if those tools are missing instead of letting `static-php-cli` mutate package-manager state during the release bundle step, avoids macOS bash empty-array expansion under `set -u` while wrapping Apple Silicon `spc` commands in `arch -arm64`, and the default `static-php-cli` checkout is pinned to a known upstream commit with an explicit override for deliberate upgrades. Release-verification PRs now run the same production bundle matrix even when the source branch is not `release/v*`, while keeping tag-state validation scoped to actual release branches. Release `v0.1.40` workflow run `26050697871` passed release packaging, artifact smoke checks, tag creation, GitHub release publication, and Homebrew formula update for all release targets, including `aarch64-apple-darwin`. The published release is tagged at commit `414c9daeb9500adea0ae95dcf36cebcb9e294d6e` and includes the Apple Silicon artifact `forkpress-aarch64-apple-darwin.tar.gz`. Trunk and release CI also run APFS sparsebundle E2E on both macOS targets with a bounded product retry and e2e-only tolerance for the known transient `hdiutil compact` unavailable condition. | Keep aarch64 macOS release and APFS sparsebundle E2E green on trunk for future releases. Add a separate product check if compact-success itself becomes release-critical. | +| Release gates | Linux, Windows, x86_64 macOS, and aarch64 macOS production bundle artifacts built in the last checked release. macOS and Linux release workflows install static PHP build prerequisites up front, `scripts/build-dist.sh` now fails before cloning/building PHP if those tools are missing instead of letting `static-php-cli` mutate package-manager state during the release bundle step, avoids macOS bash empty-array expansion under `set -u` while wrapping Apple Silicon `spc` commands in `arch -arm64`, and the default `static-php-cli` checkout is pinned to a known upstream commit with an explicit override for deliberate upgrades. Release-verification PRs now run the same production bundle matrix even when the source branch is not `release/v*`, while keeping tag-state validation scoped to actual release branches. Release `v0.1.41` workflow run `26051644782` passed release packaging, artifact smoke checks, tag creation, GitHub release publication, and Homebrew formula update for all release targets, including `aarch64-apple-darwin`. The published release is tagged at commit `8ae3a784a284dc2e9e7475318709c1edb58b6f96` and includes the Apple Silicon artifact `forkpress-aarch64-apple-darwin.tar.gz`. Trunk and release CI also run APFS sparsebundle E2E on both macOS targets with a bounded product retry and e2e-only tolerance for the known transient `hdiutil compact` unavailable condition. | Keep aarch64 macOS release and APFS sparsebundle E2E green on trunk for future releases. Add a separate product check if compact-success itself becomes release-critical. | ## Test Direction @@ -571,6 +571,6 @@ should stay focused on these areas: - Improve deterministic schema dependency planning for safe view/trigger reorderings while keeping cyclic or semantic ambiguity review-only. - Keep aarch64 macOS release artifacts and APFS sparsebundle E2E runs green for - each release. `v0.1.40` is the current published release after all five + each release. `v0.1.41` is the current published release after all five production-bundle targets stayed green and the release commit - `414c9daeb9500adea0ae95dcf36cebcb9e294d6e` was published. + `8ae3a784a284dc2e9e7475318709c1edb58b6f96` was published. diff --git a/scripts/cow/merge.php b/scripts/cow/merge.php index 7b51570a..08859513 100644 --- a/scripts/cow/merge.php +++ b/scripts/cow/merge.php @@ -10640,6 +10640,15 @@ function cow_merge_wordpress_attachment_upload_issues(string $target_db, string 'metadata_file' => (string)$metadata['file'], ]); } + } else { + $metadata_file = $metadata['file'] ?? null; + $record_issue($issues, $attachment_id, $post_title, 'plugin-wp-attachment-upload-metadata-file-drift', 'attachment metadata file field is missing or empty', [ + 'field' => '_wp_attachment_metadata.file', + 'role' => 'metadata-file', + 'attached_file' => $attached_file_raw, + 'metadata_file_present' => array_key_exists('file', $metadata), + 'metadata_file' => is_scalar($metadata_file) || $metadata_file === null ? $metadata_file : get_debug_type($metadata_file), + ], [$attached_path]); } if (isset($metadata['sizes']) && !is_array($metadata['sizes'])) { diff --git a/tests/cow/media_validator.php b/tests/cow/media_validator.php index 7fc8b00d..62c915a2 100644 --- a/tests/cow/media_validator.php +++ b/tests/cow/media_validator.php @@ -1580,7 +1580,7 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $ assert_same($result['status'], 'completed_with_conflicts', 'media validator holds incomplete generated-size metadata for review'); assert_same((int)($result['plugin_validators'] ?? 0), 1, 'media validator is discovered from mu-plugins during merge'); - assert_same((int)($result['plugin_validator_conflicts'] ?? 0), 113, 'media validator records missing required metadata, invalid metadata, invalid shapes, dimensions, image metadata, filesize and MIME drift, invalid file entries, generated-size, original-image, backup-size, missing-file, metadata-file drift, unsafe path, duplicate upload conflicts, and built-in WordPress upload conflicts'); + assert_same((int)($result['plugin_validator_conflicts'] ?? 0), 115, 'media validator records missing required metadata, invalid metadata, invalid shapes, dimensions, image metadata, filesize and MIME drift, invalid file entries, generated-size, original-image, backup-size, missing-file, metadata-file drift, unsafe path, duplicate upload conflicts, and built-in WordPress upload conflicts'); assert_same( scalar($target, "SELECT meta_value FROM wp_postmeta WHERE post_id = $attachment_id AND meta_key = '_wp_attached_file'"), '2026/05/source-generated-missing-file-key.jpg', @@ -2117,6 +2117,18 @@ function insert_attachment_with_single_meta(SQLite3 $db, string $title, string $ assert_true($url_metadata_file_recorded, 'media validator mismatch audit payload identifies the URL-like metadata file attachment'); assert_true($drive_metadata_file_recorded, 'media validator mismatch audit payload identifies the drive-letter metadata file attachment'); + $builtin_metadata_file_audit = cow_merge_audit_report($metadata, (int)$result['run_id'], 10, [ + 'scope' => 'plugin', + 'records' => 'conflicts', + 'conflict_type' => 'plugin-wp-attachment-upload-metadata-file-drift', + ]); + assert_same(count($builtin_metadata_file_audit['conflicts']), 3, 'built-in WordPress upload validator exposes mismatched, missing, and empty attachment metadata file fields'); + $builtin_metadata_file_preview = implode("\n", array_map(fn($conflict) => (string)($conflict['chosen_preview'] ?? ''), $builtin_metadata_file_audit['conflicts'])); + assert_true(str_contains($builtin_metadata_file_preview, 'source-missing-metadata-file-field.jpg'), 'built-in upload validator includes the missing metadata file field attachment'); + assert_true(str_contains($builtin_metadata_file_preview, (string)$missing_metadata_file_field_id), 'built-in upload validator includes the missing metadata file field attachment ID'); + assert_true(str_contains($builtin_metadata_file_preview, 'source-empty-metadata-file-field.jpg'), 'built-in upload validator includes the empty metadata file field attachment'); + assert_true(str_contains($builtin_metadata_file_preview, (string)$empty_metadata_file_field_id), 'built-in upload validator includes the empty metadata file field attachment ID'); + $unsafe_audit = cow_merge_audit_report($metadata, (int)$result['run_id'], 20, [ 'scope' => 'plugin', 'records' => 'conflicts',