mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-03 12:10:31 +00:00
6f8105b01dd9dcfeff4d9307bf0f40fbb0984cfa
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
53252ee5d0 |
fix(web): close BUG-2129 stale-fields gap in the collection-edit switch fence (#961)
* fix(web): close BUG-2129 stale-fields gap in the collection-edit switch fence
The EditCollectionModal onupdated handler's switch fence used
`{@const keyedSlug = itemSlug}` to detect a superseded save, but Svelte 5
compiles that as a lazily-pulled derived signal — since keyedSlug was only
read inside the async callback, it always evaluated the CURRENT itemSlug,
not the value at modal-open time, making the fence a no-op. Replace it with
a genuine gen+id snapshot (pendingCollectionEditGen/-ItemId) captured
synchronously in the onmanage click handler, and use it to (a) skip
navigation/archive/close for a genuinely superseded save and (b) still
refresh the currently-shown item's fields when it's in the collection that
just migrated, closing the stale-fields clobber gap BUG-2129 describes.
Adds two Playwright regression tests: a same-collection pane-switch repro
(BUG-2129's literal scenario) and a cross-collection full-page navigation
test that mutation-testing confirms discriminates the fix (fails on the
pre-fix code with an observable wrong-page hijack).
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
* fix(web): thread editedCollectionId through EditCollectionModal to fix overlap + rename/archive gaps
Codex review of the initial fix found two real gaps: (1) the
pendingCollectionEditGen/-ItemId snapshot was a single shared mutable slot,
so opening a second collection-edit (for a different item) while an earlier
save was still in flight would overwrite it, letting the earlier save's
completion misapply the wrong item's context; (2) the superseded-but-same-
collection branch only handled the reload case, not a pending rename
(still uses the stale collSlug -> 404) or archive (silently did nothing).
Replace the parent-side snapshot with a value EditCollectionModal itself
captures synchronously (before its API call) and echoes back through
onupdated(updated, editedCollectionId). ItemDetail's fence collapses to one
check — does the currently-shown item still belong to editedCollectionId —
applied uniformly to the reload, rename-redirect, and archive-redirect
paths, so all three now behave correctly whether or not the pane switched
items mid-save.
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
* fix(web): gate the collection-edit fence on route/load settlement + fix a PATCH-timing test race
Codex round 2 found a narrower race: between a route change (collSlug/
itemSlug updating) and loadData()'s async resolution, item/collection can
transiently still hold the PREVIOUS item's data while collSlug/itemSlug
already reflect the new one. A mixed read across that window could pass
the collection-id fence using the stale `item` but build a navigation URL
from the already-updated collSlug/itemSlug, hijacking to a mismatched URL.
Gate the whole onupdated body on the existing `itemMatchesRef` invariant
(already used elsewhere in this file for the same "has loadData() caught
up" check) — bailing there is always safe since the route's own in-flight
loadData() will fetch fresh state regardless.
Also fixes a regression-test-only flake Codex flagged: the field-update
PATCH readback used waitForRequest (resolves on dispatch) instead of
waitForResponse (resolves on commit), so the immediate belt-and-suspenders
GET could race an in-flight write.
Deferred (documented in the PR, not fixed here): a further compound race
where a still-open second collection-edit modal on a different item could
be closed by an earlier, unrelated same-collection save's completion —
narrow, pre-existing-adjacent, and squarely in PLAN-2154 Phase 1's
dedicated R14 fence-sweep scope (TASK-2167) rather than this Phase 0 fix.
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
* fix(web): allow a safe corrective reload during route transitions; sync test on PATCH response
Codex round 3: gating the ENTIRE onupdated body on itemMatchesRef (added
last round) was too broad — it also suppressed the corrective void
loadData() call during a route-transition window, reopening BUG-2129's
core symptom (an in-flight load that raced the migration and lost gets no
second chance). loadData() is idempotent and gen-fenced against any
in-flight load, so calling it again is always safe, even mid-transition.
Split the behavior: the relevance + reload decision runs unconditionally
(using whatever `item` is currently known, stale or not — false positives
just cause a harmless redundant reload), while only the RISKY navigation
(rename-redirect / archive-redirect, which builds a URL from collSlug/
itemSlug) stays gated on itemMatchesRef, since those are what a mixed
stale-item/fresh-route read could misdirect (Codex round 2's finding).
collectionStore.loadCollections(wsSlug) now runs unconditionally too,
keeping the global sidebar in sync regardless of pane relevance.
Also tightens the cross-collection test: replaces a fixed 1.5s sleep
after releasing the migration with an explicit wait for the PATCH
response, so the assertion can't false-pass by running before the
(possibly buggy) navigation had a chance to fire.
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
* fix(web): fence the collection-edit callback on collSlug instead of item state
Codex round 4: gating rename/archive redirects on itemMatchesRef (added
last round) fixed the cross-collection hijack but broke the SAME-collection
case — during a same-pane item switch, collSlug never changes, so a
pending rename/archive that resolves mid-transition would skip the needed
redirect (itemMatchesRef false) and then loadData() 404s fetching the
now-stale/gone collSlug, with nothing to correct it afterward.
Replace the item-state-based fence entirely with a comparison against
`collSlug` — a plain reactive prop derived straight from the route params,
with no async lag (item/collection require a loadData() round-trip to
catch up; collSlug updates synchronously with navigation). EditCollectionModal
now also echoes back `editedCollectionSlug` (captured the same way as
editedCollectionId). `collSlug === editedCollectionSlug` is correct in
every case with no separate itemMatchesRef gate needed: unchanged for a
same-pane switch (so rename/archive/reload all still fire correctly),
and updated immediately for a cross-collection navigation (so a stale
edit's redirect correctly no-ops). This resolves the tension between
rounds 2-4 of Codex's findings with one simpler, more robust check.
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
* fix(web): guard the collection-edit callback against a destroyed instance + workspace reuse
PR-level Codex review found two more real gaps in the fence:
1. The onupdated closure can still fire after this ItemDetail instance is
torn down entirely (pane closed, or the whole page navigated away)
while a save was pending — JS doesn't cancel a lingering promise on
unmount. Unguarded, that stale closure's goto()/collectionStore writes
would visibly affect whatever the user has since navigated to. Added a
one-way `destroyed` flag (set in onDestroy, alongside the existing
loadGeneration bump) checked first in the callback.
2. Collection slugs are workspace-scoped, so if this component instance
is ever reused across a workspace switch (no remount, same as the
existing collSlug/itemSlug reuse this fence already relies on), a
collSlug match alone can't tell two different workspaces' same-named
collections apart. EditCollectionModal now also echoes back
`editedWsSlug`, compared against the live `wsSlug` prop.
Also documents (not fixed here) a narrow pre-existing-adjacent edge case
Codex flagged: an embedded pane driven by a hand-crafted `?item=` whose
item lives in a different collection than the host page's collSlug won't
match this fence when its real collection is edited. This doesn't regress
anything (the prior fence was dead code and refreshed unconditionally
regardless of relevance) and is deferred to PLAN-2154 Phase 1 (TASK-2167).
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
|
||
|
|
5033bf6ef8 |
fix(web): harden split-pane collab teardown flush + regression e2e (TASK-2117) (#947)
Verify + harden provider/doc/flusher teardown across BOTH pane teardown
paths PLAN-2105's no-{#key} persistent pane introduced, plus the raw-mode
data-loss fix. The task anticipated a real defect ("if the child Editor
tears down before the parent collab $effect cleanup, capture the markdown
synchronously"); an independent Codex pass + an empirical Svelte probe
confirmed that IS the case.
Product hardening (ItemDetail.svelte):
- On unmount Svelte destroys the child <Editor> BEFORE this component's
top-level collab $effect cleanup (top-level $effects are deferred to
component pop(), so they tear down AFTER the template render effect that
owns <Editor> — proven by src/lib/collab/teardownOrder/order.svelte.test.ts).
So at teardown-flush time the editor is already destroyed; persistence
survived only because Tiptap happens to still serve storage after
editor.destroy() — a fragile implementation detail.
- Add `lastEditorMarkdown`, a shadow captured on every edit
(handleContentUpdate), reset per provider instance so it can't cross
items. readEditorMarkdown now prefers live storage only while the editor
is alive (!isDestroyed) and falls back to the shadow otherwise — making
the teardown flush correct-by-construction regardless of destroy order
or Tiptap's post-destroy behavior. The earlier comment (which claimed
the reverse ordering) is corrected.
Verified findings (unchanged, correct):
- Item-switch: collab $effect cleanup flushes → destroys provider+doc →
nulls slots; loadData reset + editorInstance-nulling fire.
- Raw-mode: loadData flushes the raw saver (keepalive) before
clearPending() when dirty.
- CollabProvider: per-itemID sessionStorage cursor isolates A/B; no change.
Regression e2e (pane-collab-teardown.spec.ts) — each proven to fail when
its target fix is reverted (mutation-tested), so they can't false-pass on
a timer-based backup (the risk the Codex pass flagged):
- close pane → edit reaches items.content; the collab-snapshot PATCH must
DISPATCH within 3s of close (the teardown flush), not ~5s later (the
idle backup).
- switch A->B in-pane → outgoing edit flushed (loadData cancels the idle,
so the cleanup flush is the sole path), exactly one collab WS at a time,
close → 0 WS, no A->B cross-write.
- switch mid raw-debounce → outgoing raw edit flushed (loadData cancels
the debounce, so the keepalive flush is the sole path).
Explicit 20s waits on "Synced" cover a slow handshake + reconnect backoff.
Extracts shared login/seed scaffolding into e2e/lib/collab-helpers.ts
(reused by collab-persistence.spec.ts).
Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
|
||
|
|
954c84d0bf |
refactor(e2e): extract demo data into shared module (TASK-1201) (#431)
Lift the static "realistic workspace" data out of seedRealisticContent
into web/e2e/lib/demo-data.ts so it can be consumed by pad-remotion (a
sibling repo) without dragging in Playwright as a dependency. Single
source of truth for what a real-feeling Pad demo looks like.
Wire-shape compatibility is preserved exactly:
- demoPlan: same title, status, content
- demoTasks: same 7 tasks in same order, same status/priority/effort,
same parent-to-plan linkage (now expressed via parentToPlan: boolean
rather than carrying the plan id inline — the seeder maps it back
to the freshly-created plan id at post time)
- demoIdeas: same 2 ideas, same fields
Behavior verified by running the gated screenshot spec:
PAD_SCREENSHOTS=1 npx playwright test e2e/screenshots.spec.ts \
--project=desktop-chromium
seedRealisticContent runs to completion and the dashboard / board /
list / table screenshots regenerate identically (reverted; not part
of this PR's diff).
demoConventions is also exported (4 representative entries) for the
pad-remotion ContextScene to render ghost-cards. demo-seed.ts itself
doesn't consume it — seedConventions takes caller-supplied input — but
the shape lives here so the shared data module is complete.
The companion pad-remotion file (src/data/demoItems.ts) lands as a
separate PR in PerpetualSoftware/pad-remotion. We chose copy-with-
manual-sync over a path import / symlink because pad-remotion is a
distinct git repo; a CI drift check is a possible follow-up if this
duplication starts to bite.
Parent: PLAN-1198.
|
||
|
|
cf04e16b5e |
chore(e2e): blog screenshot capture spec + shared seed helpers (TASK-1031) (#374)
Adds infrastructure for capturing Pad UI screenshots that ship inside
blog posts on getpad.dev.
* web/e2e/lib/demo-seed.ts (new) — extracts the realistic-content seed
(1 active plan + 7 tasks + 2 ideas) from screenshots.spec.ts into a
shared module, plus two new helpers:
- seedConventions(fixture, request, [...])
- activateLibraryConventions(fixture, request, titles)
Both consumers now share the same source of truth.
* web/e2e/blog-screenshots.spec.ts (new) — gated on
PAD_BLOG_SCREENSHOTS=1. One test.describe per blog post; each owns
its post-specific seed and captures into ../../pad-web/static/blog/
<slug>/. First consumer is BLOG-1007 (Conventions and Playbooks);
subsequent posts add a describe block per shot.
* web/e2e/screenshots.spec.ts — refactored to import seedRealisticContent
from the shared lib. No behavior change; PAD_SCREENSHOTS=1 README
capture still passes.
Companion publish helper lives in pad-web at scripts/blog-publish.mjs.
Capture command:
make build-go && cd web && PAD_BLOG_SCREENSHOTS=1 \
npx playwright test blog-screenshots --project=desktop-chromium
Refs TASK-1031, unblocks BLOG-1022 / BLOG-1004 / BLOG-1003 backfill
which all want screenshots.
|