Sync: NFC to NFD local rename leaves client tree divergent #169

Closed
opened 2026-09-26 12:28:45 +00:00 by kayg · 6 comments
Owner

Seeded real-server sync chaos finds a permanent tree mismatch after an NFC to NFD local rename.

Reproduce from job/sync-chaos at 0347504 or later with built calternal-server and calternald binaries:

TMPDIR=$PWD/target/tmp python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 180 --steps 6

The same seed failed three times at step 7 (unicode nfd). Before the rename, café-5.txt had converged. The originating installation renamed it to the decomposed cafe\u0301-5.txt. After 35 seconds, server API and installation 2 retained café-5.txt, while installation 1 retained cafe\u0301-5.txt. No daemon exit was observed. calternal-fs::normalize_new_name deliberately normalizes new names to NFC. The sync engine must converge the local name with the server's accepted name without losing bytes or creating a duplicate. This is a sync collision and blocks merge under CLAUDE.md.

Observed path sets: remote ['café-5.txt', ...]; local 1 ['cafe\u0301-5.txt', ...]; local 2 ['café-5.txt', ...]. The probe checks exact bytes, directories, Files Index, and both client journals on each settled step.

Found in Forgejo #163. Product fix belongs to crates/calternal-sync/src; this job owns the chaos harness and is leaving product behavior to a separate fix.

Seeded real-server sync chaos finds a permanent tree mismatch after an NFC to NFD local rename. Reproduce from job/sync-chaos at 0347504 or later with built `calternal-server` and `calternald` binaries: ``` TMPDIR=$PWD/target/tmp python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 180 --steps 6 ``` The same seed failed three times at step 7 (`unicode nfd`). Before the rename, `café-5.txt` had converged. The originating installation renamed it to the decomposed `cafe\u0301-5.txt`. After 35 seconds, server API and installation 2 retained `café-5.txt`, while installation 1 retained `cafe\u0301-5.txt`. No daemon exit was observed. `calternal-fs::normalize_new_name` deliberately normalizes new names to NFC. The sync engine must converge the local name with the server's accepted name without losing bytes or creating a duplicate. This is a sync collision and blocks merge under CLAUDE.md. Observed path sets: remote `['café-5.txt', ...]`; local 1 `['cafe\u0301-5.txt', ...]`; local 2 `['café-5.txt', ...]`. The probe checks exact bytes, directories, Files Index, and both client journals on each settled step. Found in Forgejo #163. Product fix belongs to `crates/calternal-sync/src`; this job owns the chaos harness and is leaving product behavior to a separate fix.
Author
Owner

Starting fix and reproduction on job/sync-converge, based on dev at f1c0766907d5d1a0a3a7ea0ae807e43dd2caf9f0.

Starting fix and reproduction on `job/sync-converge`, based on `dev` at `f1c0766907d5d1a0a3a7ea0ae807e43dd2caf9f0`.
Author
Owner

Follow-up from the post-#163 merge on 2026-09-27 (menu-icons HEAD e70f3d26). I ran the newly merged real-server probe once:

python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 120

It failed at step 7 (unicode nfd) after the 35-second settle window. Diagnostics showed remote café-5.txt, local 1 cafe\u0301-5.txt, local 2 café-5.txt; bytes_equal=[False, True] and journals_match=True. The failure was AssertionError: unicode nfd did not converge. The server upload metadata parser normalizes each new path component through calternal_fs::normalize_new_name, which returns NFC. This confirms the tree mismatch tracked here. No sync behavior change was made in the menu-icons job.

Follow-up from the post-#163 merge on 2026-09-27 (menu-icons HEAD `e70f3d26`). I ran the newly merged real-server probe once: ``` python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 120 ``` It failed at step 7 (`unicode nfd`) after the 35-second settle window. Diagnostics showed remote `café-5.txt`, local 1 `cafe\u0301-5.txt`, local 2 `café-5.txt`; `bytes_equal=[False, True]` and `journals_match=True`. The failure was `AssertionError: unicode nfd did not converge`. The server upload metadata parser normalizes each new path component through `calternal_fs::normalize_new_name`, which returns NFC. This confirms the tree mismatch tracked here. No sync behavior change was made in the menu-icons job.
Author
Owner

Reproduced before changes with the real-server chaos harness:

TMPDIR=$PWD/target/tmp python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 180 --steps 6

It failed at unicode nfd. Diagnostics showed the server and installation 2 had café-5.txt (NFC), while installation 1 had café-5.txt (NFD). Both journals matched the server Index, so this is a stale accepted path after the server normalized the rename, not journal lag.

Reproduced before changes with the real-server chaos harness: ``` TMPDIR=$PWD/target/tmp python3 tests/adversarial/sync_chaos.py --seed 163 --seconds 180 --steps 6 ``` It failed at `unicode nfd`. Diagnostics showed the server and installation 2 had `café-5.txt` (NFC), while installation 1 had `café-5.txt` (NFD). Both journals matched the server Index, so this is a stale accepted path after the server normalized the rename, not journal lag.
Author
Owner

Seeded chaos seed 1 reached its NFD rename at step 9. The harness reported divergence because its random Unicode assertion required the remote tree to use the NFD spelling. Evidence from the failure: remote and both local inventories all contained the NFC spelling café-7.txt, both peers had equal bytes, and journals matched. The deterministic regression and sync rule require the canonical server spelling (NFC), so this is a stale harness expectation; I am aligning the random assertion with that rule and rerunning the matrix.

Seeded chaos seed 1 reached its NFD rename at step 9. The harness reported divergence because its random Unicode assertion required the remote tree to use the NFD spelling. Evidence from the failure: remote and both local inventories all contained the NFC spelling `café-7.txt`, both peers had equal bytes, and journals matched. The deterministic regression and sync rule require the canonical server spelling (NFC), so this is a stale harness expectation; I am aligning the random assertion with that rule and rerunning the matrix.
Author
Owner

Fixed Forgejo #169: NFC to NFD local renames no longer leave an Installation with a divergent name. The sync engine now normalizes the observed local path to NFC and updates its journal and path map with the server spelling. A normalization-only rename of the same file does not send a conflicting remote rename.

Branch: job/sync-converge
Head: 39a30cf88a2c35e9e137d69563448426f0cfdcb1
Latest dev merged: 74d6072643e59cf8668a0cbd9e02b3e36b79736e

Changes:

  • NFC normalization keeps a local NFD rename on the server's canonical NFC spelling. The local file is renamed through directory handles, and the journal and path map are updated to that spelling.
  • Folder moves are sent as one folder rename when the unchanged tracked subtree is fully inside the selected sync scope. Peer Installations rename the directory safely, and all path snapshots are rebased together. If the subtree is not a complete match, sync keeps the existing item-level behavior.
  • Added real-server deterministic regressions for both cases and aligned the random Unicode chaos assertion with the server's NFC spelling.

Validation:

  • Final-head deterministic real-server replays: PASS sync chaos regression=nfd_rename; PASS sync chaos regression=folder_move.
  • Random real-server chaos on bfb78b9a: PASS sync chaos matrix seeds=1..40,163,173 count=42 (10 randomized rounds per seed, plus built-in upload and interrupted-download cases). The later dev merge did not change sync reconciliation; both deterministic replays were repeated on final head.
  • cargo fmt --all -- --check: no output.
  • cargo clippy --all-targets -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) in 12m 37s. Clippy emitted no diagnostics. The shell summary wrapper then printed zsh:1: read-only variable: status after Cargo exited.
  • cargo test: calternal-sync reported test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; finished in 1.43s; daemon tests reported test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s; command exit code 0.
  • bun run --cwd apps/web check: svelte-check found 0 errors and 0 warnings (exit 0).
  • bun run --cwd apps/web test: Test Files 80 passed (80) and Tests 585 passed (585) (exit 0).

Known gaps: Vitest printed jsdom notices (Could not parse CSS stylesheet and Not implemented: Window's scrollTo() method) but all tests passed. No sync gap remains known.

Decisions not stated in the design: the server's NFC spelling wins when a local rename changes only Unicode normalization; directory moves are treated as a unit only when every tracked descendant is unchanged and within the selected scope.

Fixed Forgejo #169: NFC to NFD local renames no longer leave an Installation with a divergent name. The sync engine now normalizes the observed local path to NFC and updates its journal and path map with the server spelling. A normalization-only rename of the same file does not send a conflicting remote rename. Branch: `job/sync-converge` Head: `39a30cf88a2c35e9e137d69563448426f0cfdcb1` Latest dev merged: `74d6072643e59cf8668a0cbd9e02b3e36b79736e` Changes: - NFC normalization keeps a local NFD rename on the server's canonical NFC spelling. The local file is renamed through directory handles, and the journal and path map are updated to that spelling. - Folder moves are sent as one folder rename when the unchanged tracked subtree is fully inside the selected sync scope. Peer Installations rename the directory safely, and all path snapshots are rebased together. If the subtree is not a complete match, sync keeps the existing item-level behavior. - Added real-server deterministic regressions for both cases and aligned the random Unicode chaos assertion with the server's NFC spelling. Validation: - Final-head deterministic real-server replays: `PASS sync chaos regression=nfd_rename`; `PASS sync chaos regression=folder_move`. - Random real-server chaos on `bfb78b9a`: `PASS sync chaos matrix seeds=1..40,163,173 count=42` (10 randomized rounds per seed, plus built-in upload and interrupted-download cases). The later `dev` merge did not change sync reconciliation; both deterministic replays were repeated on final head. - `cargo fmt --all -- --check`: no output. - `cargo clippy --all-targets -- -D warnings`: `Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 37s`. Clippy emitted no diagnostics. The shell summary wrapper then printed `zsh:1: read-only variable: status` after Cargo exited. - `cargo test`: `calternal-sync` reported `test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; finished in 1.43s`; daemon tests reported `test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s`; command exit code 0. - `bun run --cwd apps/web check`: `svelte-check found 0 errors and 0 warnings` (exit 0). - `bun run --cwd apps/web test`: `Test Files 80 passed (80)` and `Tests 585 passed (585)` (exit 0). Known gaps: Vitest printed jsdom notices (`Could not parse CSS stylesheet` and `Not implemented: Window's scrollTo() method`) but all tests passed. No sync gap remains known. Decisions not stated in the design: the server's NFC spelling wins when a local rename changes only Unicode normalization; directory moves are treated as a unit only when every tracked descendant is unchanged and within the selected scope.
Author
Owner

Merged in 235073a6.

Merged in 235073a6.
kayg closed this issue 2026-09-27 14:39:50 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#169
No description provided.