Pad/internal/test/e2e
Greg Pomerantz 6c6a0c1a27 Fix write-concurrency race: per-file write protocol + unique temps
The review-identified race: saves are async (owner snapshots content,
worker pool writes), nothing serialized per file, and every write of a
file used the SAME deterministic temp ('.<name>.tmp'). Two overlapping
writes (autosave x autosave, retry x autosave, or the synchronous
FlushAll on GoToBrowser/Shutdown x a worker write) interleaved on the
shared temp and could rename a byte-mixture into place; even without
interleaving, last-rename-wins could promote a STALE snapshot.

Owner-side protocol (logic.go, requestSave + result handler):
- at most one write in flight per file (writeInFlight maps filename ->
  the file version whose content the in-flight write carries);
- a save requested while one is in flight is deferred (savePending) and
  re-issued by the write's result handler with a FRESH snapshot, so
  'last rename wins' coincides with 'newest snapshot wins';
- on success the SNAPSHOT version (not the current one) is recorded as
  written, so an edit that arrived during the write leaves the file
  dirty and triggers the re-issue;
- FlushAll (GoToBrowser, Shutdown) defers via the same protocol instead
  of writing concurrently on the shared temp;
- shutdown drain: on done the owner waits (bounded 5 s) for in-flight
  writes and armed retries to settle before exiting, so the post-exit
  synchronous FlushAll and workerPool.Stop cannot race a straggling
  worker write;
- retry timer now sends a non-blocking token (no timer-goroutine stall
  on a full channel); emitFrame no longer blocks on a slow/gone main
  (frames are snapshots; the next emission wins) - also required so the
  drain can never deadlock on frame delivery.

Mechanism (real/filesystem.go):
- WriteFileAtomic uses a unique per-call temp ('.<name>.tmp.<pid>.<seq>'),
  making same-file staging-file interleaving structurally impossible even
  if the serialization regressed (defense in depth);
- each successful write best-effort removes stale temps of the same file
  (crash leftovers, plus the legacy deterministic name for upgraded
  installs); a failed write removes its own temp.

Tests:
- write_serialization_test.go (e2e): a counting FS wrapper proves the
  peak concurrent same-file saves is 1 across two deliberately
  overlapping autosaves (2 s saves; the second edit lands inside the
  first save's window and its token is deferred, then re-issued with the
  newer content), and that a Flush during an in-flight save adds no
  concurrent writer and the newest snapshot still wins. Mutation-verified:
  disabling the deferral fails it with peak = 2. (The pool's WriteFileTask
  calls FS.WriteFile, not WriteFileAtomic - the real FS is atomic only
  because WriteFile delegates to WriteFileAtomic; the wrapper mirrors that
  delegation or the overlap window does not exist.)
- filesystem_test.go: stale-temp test updated to the new pattern, also
  covering the legacy name and asserting a different file's temp is
  untouched.
- real_file_fuzz_test.go: stray-temp check matches both patterns.

Docs: architecture.md 6.5 rewritten (protocol invariants), spec.md
autosave line, development_plan.md v11 + Phase 12.

On-device smoke: open, type, autosave lands exact content on disk, no
temp files left, clean relaunch. Full suite green under -race.

Residuals (documented): no fsync before rename (power-loss window only);
external-change detection absent; a drain-deadline exit with a straggling
write can only lose freshness (unique temps keep every rename a complete
snapshot).
2026-08-17 16:29:47 -04:00
..
assertions.go Add e2e test harness for testing editor logic without Gio display 2026-05-31 08:56:30 -04:00
browser_files_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
capture.go Add e2e test harness for testing editor logic without Gio display 2026-05-31 08:56:30 -04:00
cursor_interaction_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
edit_lifecycle_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
harness_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
harness.go Add text selection, real-file e2e tests, and Android arrow-key support 2026-08-17 00:32:53 -04:00
helpers.go Add e2e test harness for testing editor logic without Gio display 2026-05-31 08:56:30 -04:00
real_file_chunk_boundary_test.go Add text selection, real-file e2e tests, and Android arrow-key support 2026-08-17 00:32:53 -04:00
real_file_edit_test.go Add text selection, real-file e2e tests, and Android arrow-key support 2026-08-17 00:32:53 -04:00
real_file_fuzz_test.go Fix write-concurrency race: per-file write protocol + unique temps 2026-08-17 16:29:47 -04:00
scroll_cursor_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
search_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
sort_mode_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
touch_selection_e2e_test.go Touch selection (v1): long-press/double-tap word selection, drag handles, floating copy/cut/paste menu 2026-08-17 08:57:55 -04:00
type_at_start_test.go Make state single-owner and stabilize race detector 2026-08-16 01:32:27 -04:00
write_serialization_test.go Fix write-concurrency race: per-file write protocol + unique temps 2026-08-17 16:29:47 -04:00