Skip to content

fix(ui): tombstone optimistic kept entries (#113 partial)#115

Merged
iduartgomez merged 3 commits intomainfrom
fix/click-to-read-kept-rebuild-113
May 5, 2026
Merged

fix(ui): tombstone optimistic kept entries (#113 partial)#115
iduartgomez merged 3 commits intomainfrom
fix/click-to-read-kept-rebuild-113

Conversation

@iduartgomez
Copy link
Copy Markdown
Contributor

Summary

Refs #113. Partial fix — the kept_for race documented in #113 is real and now has both unit-test coverage and a working repair. However, end-to-end verification on the iso harness still trips on a separate duplicate-inbox bug filed as #114, so the multi-round Playwright test still fails. Landing this anyway because the race fix is independently correct.

What changed

  • Adds `PENDING_KEPT` tombstone map in `ui/src/local_state.rs`, mirroring the `DELETED_DRAFTS` shape from Sent messages leaking into Drafts folder after cross-node send #107. `local_mark_read` writes optimistically to `SNAPSHOT.kept` and stashes a tombstone keyed by `(alias, msg_id)`. On every `replace_snapshot`:
    • if the delegate's echo includes the kept entry, the round-trip is durable → drop the tombstone.
    • if not, this is a stale echo from before the write → re-merge the optimistic kept entry into the new snapshot so `kept_for` keeps surfacing the row. Re-applies `read=true` too.
  • `local_archive_message` and `local_delete_message` drop the pending tombstone so a stale echo doesn't resurrect kept rows under archived/deleted ids.
  • 4 new unit tests pin the new behavior. The previously-`#[ignore]`'d `replace_snapshot_does_not_clobber_optimistic_kept` is now active and passes.
  • `bump()` is now a runtime-aware no-op when called outside Dioxus, so the SNAPSHOT mutators can be exercised in plain unit tests without mounting an app.
  • Adds Playwright tracing + per-context console mirroring to the multi-round live-node spec so createIdentity flow registers multiple inbox contracts under the same alias #114 (and any future regression) has artifacts useful for diagnosis.

Why this is partial

After landing the fix, the iso harness multi-round test still fails the same way. Bob's UI is subscribed to two distinct inbox contracts both registered against `alias=bob` — see #114 for the gateway-log evidence. `mark_as_read` then targets the wrong inbox via `inbox_data[active_id]`, and the row disappears regardless of `kept_for`. The race fix is necessary but not sufficient for the full click-to-read end-to-end story.

Test plan

  • `cargo test -p freenet-email-ui --lib --no-default-features --features example-data,no-sync local_state::tests::` — all 8 tests pass
  • `cargo clippy -p freenet-email-ui --lib --no-default-features --features example-data,no-sync --tests` — clean
  • `cargo check -p freenet-email-ui --lib --target wasm32-unknown-unknown` — clean
  • `FREENET_LIVE_E2E_SEND=1 cargo make test-e2e-real-node` test 3 passes — blocked on createIdentity flow registers multiple inbox contracts under the same alias #114

Adds 5 unit tests in local_state and 1 #[ignore]'d failing test that
reproduces the click-to-read regression (#113). Stale GetAll echoes
hit replace_snapshot before the optimistic local_mark_read write
round-trips through the delegate, so kept_for returns empty and the
just-evicted row vanishes from the inbox list.

Also makes bump() a runtime-aware no-op when called outside Dioxus
so the SNAPSHOT mutators can be exercised in plain unit tests.

Adds Playwright tracing + per-context console mirroring to the
multi-round live-node spec so the next iso-harness CI run captures
artifacts useful for the fix follow-up.

Refs #113, #81
Mirrors the DELETED_DRAFTS pattern from #107: every local_mark_read
write also lands in PENDING_KEPT, keyed by (alias, msg_id). On every
replace_snapshot, the tombstone walks the incoming snapshot:

  - if the delegate's echo already contains the kept entry, the
    MarkRead round-trip is durable — drop the tombstone.
  - if not, this is a stale echo from before the write — re-merge the
    optimistic kept entry (and re-apply read=true) into the new
    snapshot so kept_for keeps surfacing the row.

Without this, a click-to-read on a cross-node delivery left the row
gone from the inbox list: the inbox contract had already evicted it
(by design — read messages move out of the live mailbox), and a stale
GetAll reply landing after the click cleared SNAPSHOT.kept before
the MarkRead delegate write had echoed back. kept_for then returned
empty and the user-visible row vanished (#113).

archive and delete supersede kept, so both drop the pending tombstone
to avoid resurrecting the kept row under an archived/deleted id on a
late echo.

Adds 4 tests pinning the new behavior:
  - replace_snapshot_does_not_clobber_optimistic_kept (the #113 race;
    previously #[ignore]'d as failing)
  - pending_kept_drops_after_delegate_echo_includes_entry
  - local_archive_drops_pending_kept_tombstone
  - local_delete_drops_pending_kept_tombstone

Closes #113
@iduartgomez iduartgomez merged commit ffe5f42 into main May 5, 2026
5 checks passed
@iduartgomez iduartgomez deleted the fix/click-to-read-kept-rebuild-113 branch May 5, 2026 22:22
iduartgomez added a commit that referenced this pull request May 6, 2026
…on (#81) (#123)

#114 + #115 + #120 fixes plus a spec correction (use page.reload()
instead of page.goBack(), since the SPA doesn't push history entries
on click — goBack landed on about:blank rather than the inbox)
make tests 2 + 3 deterministically green on the iso harness. Drop
the FREENET_LIVE_E2E_SEND gate so they run by default.

Two assertions stay gated as separate concerns:

- Test 3 round 3 (alice → bob retry) gated on
  FREENET_LIVE_E2E_AFT_CAP_RAISED — AFT day-1 cap is 1 slot, alice
  burned hers in round 1; needs #85 to make tier configurable.
- Test 3 round 2 (bob → alice reply) gated on FREENET_LIVE_E2E_REPLY
  — flaky ~33%, separate flake tracked in #122. Click-to-read +
  reload assertion (the original #113 target) still runs and passes
  deterministically.

Verified locally via 3 consecutive `cargo make test-e2e-real-node`
runs, all passing in ~12s each.
iduartgomez added a commit that referenced this pull request May 6, 2026
* test(e2e): flip FREENET_LIVE_E2E_SEND off + fix click-to-read assertion (#81)

#114 + #115 + #120 fixes plus a spec correction (use page.reload()
instead of page.goBack(), since the SPA doesn't push history entries
on click — goBack landed on about:blank rather than the inbox)
make tests 2 + 3 deterministically green on the iso harness. Drop
the FREENET_LIVE_E2E_SEND gate so they run by default.

Two assertions stay gated as separate concerns:

- Test 3 round 3 (alice → bob retry) gated on
  FREENET_LIVE_E2E_AFT_CAP_RAISED — AFT day-1 cap is 1 slot, alice
  burned hers in round 1; needs #85 to make tier configurable.
- Test 3 round 2 (bob → alice reply) gated on FREENET_LIVE_E2E_REPLY
  — flaky ~33%, separate flake tracked in #122. Click-to-read +
  reload assertion (the original #113 target) still runs and passes
  deterministically.

Verified locally via 3 consecutive `cargo make test-e2e-real-node`
runs, all passing in ~12s each.

* feat(ui): refuse-with-error on alias collision in Create/Restore (#117)

Before submitting a NodeAction::CreateIdentity for a new alias name,
check Identity::get_alias(name). If it returns Some, refuse with an
inline error banner instead of advancing through keygen → reveal →
delegate write.

Two paths are guarded:

  - CreateAliasForm::submit — Create new identity. Rejects before
    keygen (no burned keys for a refused name).
  - ImportForm::onclick (Restore) — refuses to overwrite an existing
    identity record from a backup file, since the backup's keys would
    silently replace the on-device delegate entry and orphan the
    previous inbox.

Mirrors the existing rename collision guard at
\`Identity::rename_in_place\`. The two flows are now consistent:
explicit two-step (delete then create/restore) is required to swap an
alias, and there is no UI path that silently destroys access to a
prior inbox.

Adds an offline-mode Playwright test that submits "address1" — already
seeded — and asserts the banner appears and the fingerprint reveal
does not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant