From 6d680ab451edc0ba0ab179347049f21b930918c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 16 May 2026 10:10:55 +0200 Subject: [PATCH] Expand focused COW stale audit gate --- docs/merge-reliability.md | 2 +- tests/cow/stale_audit.php | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/merge-reliability.md b/docs/merge-reliability.md index 7d102cb5..86a3cf07 100644 --- a/docs/merge-reliability.md +++ b/docs/merge-reliability.md @@ -24,7 +24,7 @@ when there is a test or document that exercises the specific merge invariant. | 5. Crash consistency across DB/files/metadata/Git | `docs/merge-crash-consistency.md` lists the covered boundaries. `tests/cow/merge.php` covers target DB, metadata, file, rollback-failure, ID-band, and whole-branch rollback paths. `tests/cow/e2e.sh` drives public merge/create/reset/recover crash/retry flows for DB, metadata, before-file, after-file, recovery-cleanup, branch-birth, branch-reset publication failpoints, and actual smart-HTTP Git-created branch pushes interrupted before branch-birth metadata, before branch-list publication, and after branch-list publication, each verified after a fresh server restart. `tests/cow/git_server.php` covers Git-created branch birth, Git update/delete, stale cleanup, and object-prune interruption. | Broaden external kill harness coverage across the remaining Git-push failpoints and platform-specific APFS/cleanup checkpoints, then verify post-crash state from a fresh process. | | 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, 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, 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, reset/reuse protection that allocates fresh bands when a branch DB drops below its old reservation, and review-held non-AUTOINCREMENT `INTEGER PRIMARY KEY` plugin collisions. `tests/cow/explicit_ids.php` fast-gates out-of-band AUTOINCREMENT inserts and primary-key rewrites for WordPress and plugin tables, including paired source deletes held behind explicit inserts. `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, source/target drift checks, plugin validator replacement evidence, and plugin replacement conflict links. `tests/cow/stale_audit.php` is a focused fast gate for reviewed cell conflicts, target drift detection, source cell/row drift detection, `needs-action` carry-forward, idempotent revalidation, audit-visible revalidation classes, and guarded `--after-revalidate` source resolution. `tests/cow/merge.php` covers stale row/cell/file drift, source drift, deleted targets, no-PK rowid replacement, supported WordPress semantic fingerprints, guarded resolution, 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 broader plugin/schema source-drift evidence, more custom logical-identity classifiers, and guarded plugin/schema-specific resolution flows where appropriate. | +| 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, source/target drift checks, plugin validator replacement evidence, and plugin replacement conflict links. `tests/cow/stale_audit.php` is a focused fast gate for reviewed cell conflicts, target drift detection, source cell/row drift detection, WordPress semantic replacement classified as `incompatible`, `needs-action` carry-forward, idempotent revalidation, audit-visible revalidation classes, and guarded `--after-revalidate` source resolution. `tests/cow/merge.php` covers stale row/cell/file drift, source drift, deleted targets, no-PK rowid replacement, supported WordPress semantic fingerprints, guarded resolution, 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 broader plugin/schema source-drift evidence, more custom logical-identity classifiers, and guarded plugin/schema-specific resolution flows where appropriate. | | 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. Trunk CI run `25952935740` passed `Build production dist bundle` and release-built COW APFS sparsebundle E2E on `aarch64-apple-darwin` and `x86_64-apple-darwin`, plus Linux COW E2E and Windows checks. The mac APFS e2e now 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 before treating Mac artifacts as trustworthy; do not treat a transient compact skip as proof that compaction itself succeeded. | ## Current Guarantees diff --git a/tests/cow/stale_audit.php b/tests/cow/stale_audit.php index 67bc2218..cc626cd8 100644 --- a/tests/cow/stale_audit.php +++ b/tests/cow/stale_audit.php @@ -290,6 +290,59 @@ function create_stale_audit_db(string $path): void { 'source payload changed after latest merge revalidation', 'after-revalidate row resolution fails if the source row changed after review' ); + + $post_identity_base = $tmp . '/post-identity-base.sqlite'; + $post_identity_source = $tmp . '/post-identity-source.sqlite'; + $post_identity_target = $tmp . '/post-identity-target.sqlite'; + $post_identity_metadata = $tmp . '/.forkpress/cow/merge/post-identity-metadata.sqlite'; + foreach ([$post_identity_base, $post_identity_source, $post_identity_target] as $path) { + $db = open_db($path); + $db->exec('CREATE TABLE wp_posts (ID INTEGER PRIMARY KEY, post_type TEXT, post_title TEXT, post_content TEXT)'); + $db->close(); + } + $db = open_db($post_identity_source); + $db->exec("INSERT INTO wp_posts (ID, post_type, post_title, post_content) VALUES (10, 'page', 'Source page', 'source page content')"); + $db->close(); + $db = open_db($post_identity_target); + $db->exec("INSERT INTO wp_posts (ID, post_type, post_title, post_content) VALUES (10, 'page', 'Target page', 'target page content')"); + $db->close(); + + $post_identity_merge = cow_merge_databases($post_identity_base, $post_identity_source, $post_identity_target, $post_identity_metadata, 'feature-post-identity-review', 'main'); + $post_identity_run_id = (int)$post_identity_merge['run_id']; + assert_same($post_identity_merge['status'], 'completed_with_conflicts', 'post semantic identity fixture starts with a same-ID row conflict'); + $post_identity_conflict_id = (int)scalar($post_identity_metadata, "SELECT id FROM merge_conflicts WHERE table_name = 'wp_posts' AND conflict_type = 'row-insert-collision'"); + assert_true($post_identity_conflict_id > 0, 'post semantic identity fixture records the row conflict'); + cow_merge_review_record( + $post_identity_metadata, + 'conflict', + $post_identity_conflict_id, + 'reviewed', + 'Review source page before applying over target page.', + 'cow-test' + ); + + $db = open_db($post_identity_target); + $db->exec("UPDATE wp_posts SET post_type = 'attachment', post_title = 'Target attachment', post_content = 'target attachment content' WHERE ID = 10"); + $db->close(); + + $post_identity_revalidated = cow_merge_revalidate_reviewed_conflicts($post_identity_metadata, $post_identity_run_id, 'cow-revalidate'); + assert_same($post_identity_revalidated['checked'], 1, 'post semantic revalidation checks the reviewed row conflict'); + assert_same($post_identity_revalidated['stale'], 1, 'post semantic revalidation detects target semantic replacement'); + assert_same($post_identity_revalidated['carried'], 1, 'post semantic revalidation carries semantically replaced rows to needs-action'); + assert_same( + scalar($post_identity_metadata, "SELECT revalidation_class FROM merge_revalidations WHERE conflict_id = $post_identity_conflict_id ORDER BY id DESC LIMIT 1"), + 'incompatible', + 'post semantic revalidation classifies changed post_type as incompatible' + ); + $post_identity_audit = cow_merge_audit_report($post_identity_metadata, $post_identity_run_id, 10, ['records' => 'conflicts']); + $post_identity_conflicts = array_values(array_filter($post_identity_audit['conflicts'], fn($row) => (int)($row['id'] ?? 0) === $post_identity_conflict_id)); + assert_same($post_identity_conflicts[0]['revalidation_class'] ?? null, 'incompatible', 'post semantic audit exposes incompatible replacement'); + assert_true(str_contains((string)($post_identity_conflicts[0]['stale_reason'] ?? ''), 'semantic identity'), 'post semantic stale reason explains semantic identity drift'); + assert_throws( + fn() => cow_merge_resolve_conflict($post_identity_metadata, $post_identity_conflict_id, 'source', true, 'Do not apply source page over replacement attachment.', 'cow-test', true), + 'latest merge revalidation is incompatible', + 'after-revalidate blocks source resolution over an incompatible post semantic replacement' + ); } finally { remove_tree($tmp); }