Repository navigation
Conversation
…ase 3, Step 2) With collaborative_script_editing on, ConfigScript renders a new CollabScriptEditor instead of the classic ScriptEditor (which is untouched). An editor joins the shared draft and browses it; everyone else reads saved pages over REST as before. - Join follows the server's editors list containing our own id (the server commits is_editor before broadcasting GET_SCRIPT_CONFIG_STATUS), so it also covers a reload while editing. Stop Editing leaves the room and sends STOP_SCRIPT_EDIT. - An editor with no room (rejected join, ROOM_CLOSED) gets an alert with Retry instead of a dead end. - Lines come from the store's page snapshots through the draft adapter, keyed by _uid so a save's id patch (or another editor's insert) never remounts a row. - Navigation stops at the server's empty trailing page: Next Page is disabled there and Go to Page past it is refused with a message. A remembered page past the end is clamped. - useDraftPages composable (page numbers, max page, lines, previous line across pages); typed setting and isCollabScriptEditing getter; requestEditFailure now shows the server's reason. - Cuts is disabled in collaborative mode until Step 5. Tests: 22 new Vitest (composable, store, component with a mutation check that index keys fail the _uid test) and a new Playwright spec 15 (setting driven through the API, restored in afterAll): 400 on a non-boolean, 409 while editing, open draft, last-page navigation, reload rejoin, second browser, stop editing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
) E2E verification of the previous commit surfaced two real issues, both fixed here: - Two locator collisions with app-wide elements, not caused by duplication I introduced: button:has-text("Edit") also matches the Stage Direction Styles sub-tab's own Edit button (scope to .sticky-header); #page-input is not unique app-wide since App.vue always mounts its own hidden global "Go to Page" modal on every route (scope to .modal.show, the pattern spec 10 already uses for the classic editor's identical modal). - A full page reload does not currently preserve is_editor/is_cutting server-side. Traced to shared, pre-existing WS session-lifecycle code (REFRESH_CLIENT reads the wrong Session row; on_close deletes the old row before the new connection's REFRESH_CLIENT can resume it) with zero prior test coverage anywhere in the codebase - genuinely never exercised before this spec reloaded mid-edit. No draft data is lost (the room rebuilds from its checkpoint on the next join), only the UI's editor status, so the user needs to click Edit again. Filed as #1419 and background-investigated (confirmed empirically via two new xfail backend integration tests, revealing a second independent race the issue hadn't identified; correctly left unfixed pending a real design decision rather than a guessed patch). The spec's reload test and CollabScriptEditor's onMounted comment now describe the real current behaviour instead of the originally assumed automatic rejoin; the rejoin code path itself is untouched and will start working with no client change once #1419 lands. Verified: client-v3 231 Vitest, tsc, eslint, prettier clean; full Playwright suite green in both chromium (229/229) and firefox (229/229, after one unrelated flaky retry of the pre-existing #1412 RBAC spec). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
…-v3-p3-step2-lifecycle
Client V3 Test Results243 tests 243 ✅ 1s ⏱️ Results for commit 86b268f. ♻️ This comment has been updated with latest results. |
Client Test Results95 tests 95 ✅ 0s ⏱️ Results for commit 86b268f. ♻️ This comment has been updated with latest results. |
Python Test Results 1 files 1 suites 49s ⏱️ Results for commit 86b268f. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (chromium)230 tests 230 ✅ 2m 21s ⏱️ Results for commit 86b268f. ♻️ This comment has been updated with latest results. |
Playwright E2E Results (firefox)230 tests 230 ✅ 2m 21s ⏱️ Results for commit 86b268f. ♻️ This comment has been updated with latest results. |
Tim020
left a comment
There was a problem hiding this comment.
Review round 1
Head reviewed: 526bdb2d5ef541a82051baba38b280e519125bc4
Verdict: changes needed. One lifecycle bug on Stop Editing (the server never closes or checkpoints the room) and one stuck-spinner failure path should be fixed before merge. The rest is test hardening and comment accuracy.
Previous round follow-up
First round: no earlier threads.
SonarQube
Source: the SonarCloud check run and bot comment on this PR. Quality Gate passed: 0 new issues, 0 accepted, 0 security hotspots, 0.0% duplication on new code. Coverage on new code shows 0.0% but passes, so the gate appears not to enforce coverage for client-v3. Nothing is needed to pass.
CI note
E2E Tests (firefox) failed, but not because of this PR. The failure is in 13-live-show.spec.ts ("can add an individual cue from the live view"), a known leader/follower race fixed by #1425, which was merged to dev after this branch was pushed. Because the suite is serial, the new spec 15 was skipped on Firefox in CI, so it has not yet run in Firefox CI. Merging dev and re-running should give a real Firefox result for spec 15.
New findings
- Critical (1):
stopEditingsends LEAVE before STOP, so the server skipscheckpointandclose_active_roomfor the last editor. It also flashes the "Could not open the script draft" alert. - Important (6):
- A failed initial
yjsSyncapply (or a join that never gets an answer) hangs on the spinner with Retry dead. requestEditignoressendObj'sfalsereturn.- The e2e
afterAllcan't restore the setting after a mid-edit failure, and its comment is inaccurate. - The reload e2e test pins #1419 and doesn't check "same draft".
- The two-browser e2e test doesn't prove the browsers share a draft.
- The unit tests don't assert the leave message or drive the real failure handlers.
- A failed initial
- Suggestion (5): silent Go to Page failures; "Step 2" comment rot;
previousLineOfallocating per render; brittle Prev/Escape e2e steps plus the "buttonless" comment; the incomplete reason list inrequestEditFailure.
Not anchored inline:
- No user docs were added under
docs/. That's fine while the setting stayshide_from_ui, but it will need docs and screenshots before it's exposed. onMountedhas no try/catch around its loads, so a network-levelfetchrejection leaves the loading spinner up forever. This is minor because the store loaders already handle non-OK responses.
Strengths
- Choosing the editor with a store getter keeps
ScriptEditor.vueuntouched; the only change the classic editor sees is the more helpfulrequestEditFailurereason. - Rows are keyed by
_uid, and a unit test proves an index key would have failed. That's the right call ahead of Step 3's id-patching. - Double joins are guarded: every join path goes through
retryJoin(), which checksisDraftActivefirst, so the watcher andonMountedcan't both join. - Next Page and Go to Page stop at the server's trailing page instead of letting a client create a page.
- The #1419 write-up in the PR and the inline comment accurately describe the server behaviour; both were verified against
ws_controller.py.
🤖 Generated with Claude Code
| function stopEditing(): void { | ||
| draftStore.leaveScriptRoom(); | ||
| sendObj({ OP: 'STOP_SCRIPT_EDIT', DATA: {} }); | ||
| } |
There was a problem hiding this comment.
Critical: Stop Editing leaves the room before telling the server, so the server never checkpoints or closes the room.
leaveScriptRoom() sends LEAVE_SCRIPT_ROOM first. The server's handler (ws_controller.py, LEAVE_SCRIPT_ROOM) only calls room.remove_client(self). When STOP_SCRIPT_EDIT arrives next, room_manager.get_room_for_client(self) returns None, because get_room_for_client only matches clients still in room.clients. So this whole block is skipped:
if not room.has_editors:
if room._dirty: await room_manager._checkpoint_room(room); ... GET_SCRIPT_REVISIONS
await room_manager.close_active_room()Scenario: A and B edit, B stops, then A (the last editor) clicks Stop Editing. The room stays open on the server with no members. The dirty state isn't checkpointed at stop time, and ROOM_CLOSED / GET_SCRIPT_REVISIONS are never broadcast. Only the periodic maintenance checkpoint protects the draft until someone rejoins. This will also matter for keeping the draft on stop in Step 4.
It also causes a visible glitch: status goes 'idle' immediately while isEditor is still true, so the red "Could not open the script draft" + Retry alert (joinFailed) flashes until GET_SCRIPT_CONFIG_STATUS arrives.
Fix: send STOP only and let the existing paths tear down locally:
- If we were the last editor, the server sends
ROOM_CLOSED, which calls_teardown(). - Otherwise the
isEditorwatcher (L256) callsleaveScriptRoom(), which is safe when already inactive.
Also check the send result (see the requestEdit comment). This needs import { toast } from '@/js/toast'.
| function stopEditing(): void { | |
| draftStore.leaveScriptRoom(); | |
| sendObj({ OP: 'STOP_SCRIPT_EDIT', DATA: {} }); | |
| } | |
| function stopEditing(): void { | |
| // STOP first: the server only demotes, checkpoints and closes the room for a client that is | |
| // still in it. ROOM_CLOSED (last editor) or the isEditor watcher then tears down locally. | |
| if (!sendObj({ OP: 'STOP_SCRIPT_EDIT', DATA: {} })) { | |
| toast.error('Cannot stop editing: not connected to the server'); | |
| } | |
| } |
The unit test at CollabScriptEditor.test.ts:275 only asserts that STOP was sent. It should also assert that no LEAVE_SCRIPT_ROOM precedes it.
🤖 Generated with Claude Code
There was a problem hiding this comment.
Confirmed against ws_controller.py and fixed exactly as suggested, in 86b268f: stopEditing() now sends STOP_SCRIPT_EDIT only. LEAVE_SCRIPT_ROOM first really did make get_room_for_client return None on the follow-up STOP_SCRIPT_EDIT, skipping checkpoint/close/broadcast for the last editor — verified by reading both handlers. Local teardown now happens only via ROOM_CLOSED (last editor) or the isEditor watcher (another editor still present), both already in place. Also added the sendObj return check and toast you suggested.
Test added: "Stop Editing sends STOP only — never LEAVE first" asserts sentOps() equals exactly ['STOP_SCRIPT_EDIT'], which would have caught this. Two more cover the two real follow-up paths (ROOM_CLOSED, losing editor status via the config-status update).
There was a problem hiding this comment.
Verified: the server-side bug is fixed. stopEditing() (L248-256) sends only STOP_SCRIPT_EDIT, so get_room_for_client still finds us, and the last editor's stop now checkpoints and calls close_active_room(). The new test (sentOps() equals exactly ['STOP_SCRIPT_EDIT']) locks that in, and spec 15's afterAll now relies on it correctly.
The alert flash I mentioned is still there (my round-1 comment wrongly implied the reordering would remove it). Fixing it is optional and not a merge blocker. For the common single-editor case, the server's STOP handler does this in order:
- It calls
close_active_room(), which sendsROOM_CLOSED. - Then it broadcasts
GET_SCRIPT_CONFIG_STATUS, which the client turns into a REST fetch.
Between the two, roomClosed() has torn down to 'idle' while isEditor is still true. So joinFailed (L181) shows the red "Could not open the script draft" alert with Retry and no reason, next to the "Collaborative draft closed" toast, until the REST round-trip lands. Your own test "ROOM_CLOSED after a successful sync also lands on Retry" asserts exactly this state. If the user clicks Retry during that window, they rejoin a room they just asked to leave.
A small local flag would fix it:
const stopRequested = ref(false);
const joinFailed = computed(
() => loaded.value && isEditor.value && !stopRequested.value && draftStore.status === 'idle'
);
// stopEditing(): on a successful send, set stopRequested.value = true
// isEditor watcher: when it turns false, reset stopRequested.value = falseAdd an assertion to "Stop Editing as the last editor" that .alert-stub does not show after roomClosed(). I'm leaving this thread open for that last piece.
🤖 Generated with Claude Code
…-v3-p3-step2-lifecycle
…ilures, test hardening - Critical: stopEditing() sent LEAVE_SCRIPT_ROOM before STOP_SCRIPT_EDIT, so the server's get_room_for_client(self) returned None on STOP and skipped checkpoint + close_active_room for the last editor entirely - the room stayed open, dirty, and unannounced. Now sends STOP only; ROOM_CLOSED (last editor) or the isEditor watcher (another editor still present) tears the local draft down, matching what the server actually does. Also fixed the same silently-ignored-sendObj-return bug in requestEdit. - Important: a failed initial yjsSync apply left status stuck at 'joining' forever - joinFailed only fires on 'idle' and retryJoin() is a no-op while isDraftActive, so there was no way out but Stop Editing. yjsSync now unwinds exactly as collabError already does for a rejected join. Added a 15s join watchdog (mirrors the existing save watchdog) covering a JOIN_SCRIPT_ROOM the server never answers at all. - Suggestion: Go to Page now sets pageError for an invalid/out-of-range number instead of doing nothing, and a rejected navigation (network failure) sets an error instead of leaving an unhandled rejection with the modal stuck open. "Step 2: read-only" reworded so it won't go stale when Step 3 lands. - E2E (15-collaborative-script-editing.spec.ts): afterAll now stops editing on both browser contexts before polling the settings PATCH, since the server refuses the mode change while anyone is still an editor; header comment corrected (retry hooks only restore state ahead of a retry, not a final failed attempt). The reload test captures and compares draft content before/after instead of only checking a row exists, and is commented as a deliberate tripwire for #1419 (also noted there). The two-browser test now proves the browsers share a room (matching content, matching last-page) rather than just that each renders something. The Next-Page test waits for the modal to actually close and navigates back to page 1 via Go to Page instead of assuming the fixture is exactly two pages long. - Unit tests: Stop Editing is now asserted to send STOP only (would have caught the ordering bug), plus its two real follow-up paths (ROOM_CLOSED, losing editor status) and the not-connected case; unmount asserts LEAVE_SCRIPT_ROOM is actually sent; the Retry alert is now driven through the real collabError/yjsSync-failure/ roomClosed handlers rather than a forced _teardown(); new scriptDraft.ts tests for the join watchdog and the failed-sync unwind; new ConfigScript.test.ts asserting which editor renders for each setting value. - requestEditFailure's comment corrected: it also fires for REQUEST_SCRIPT_CUTS failures, not just edit ones. Verified: backend 934 (unchanged), client-v3 243 Vitest (+18), tsc/eslint/prettier/ build clean, full Playwright green in both chromium and firefox (230/230 each, no retries), after merging the latest dev (E2E port isolation + the #1425 Firefox leader/follower fix). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8
|
|
Review round 1 addressed in Critical, fixed: Important, all fixed:
Suggestions, all taken: Go to Page now reports invalid input and failed navigation instead of doing nothing; the "Step 2" comment is reworded so it won't go stale; the brittle Prev-Page/Escape E2E steps and the modal-comment wording are fixed; Verification: backend 934 (unchanged), client-v3 243 Vitest (+18), tsc/eslint/prettier/build clean, full Playwright green in both chromium and firefox — 230/230 each, no retries needed — after merging the latest |
Tim020
left a comment
There was a problem hiding this comment.
Review round 2
Head reviewed: 86b268f9fa68a35171c1059700b122de7ac0a94d. The incremental diff is from 526bdb2d. Round 1's SHA is an ancestor, so this is not a force-push. The range also includes a merge of dev (#1420, #1423, #1425), which was already reviewed on its own PRs and is not re-reviewed here.
Verdict: ready to merge. The Critical lifecycle bug and every Important finding are fixed and verified against the code. One optional UX fix is left open, and it doesn't block merging.
Previous round follow-up
- Verified fixed and resolved (10): stuck spinner and join watchdog (thread),
requestEditsend check (thread), Go to Page errors (thread), "Step 2" comment (thread),afterAllordering (thread), reload "same draft" check and #1419 tripwire (thread), two-browser shared room (thread), brittle e2e steps (thread), unit-test coverage (thread),requestEditFailurecomment (thread). - Fix incomplete (1, non-blocking): Stop Editing (thread). The server-side bug is fixed and verified: STOP-only, so the last editor's stop checkpoints and closes the room. The red "Could not open the script draft" alert still flashes when the last editor stops, because
ROOM_CLOSEDarrives before theGET_SCRIPT_CONFIG_STATUSrefresh clearsisEditor. My round-1 comment wrongly implied the reordering would remove this. The thread has a suggested fix using astopRequestedflag. - Deferred by agreement (1):
previousLineOfper-render allocation moves to Step 3 (thread). - Still open without reply: 0. Needs human decision: 0.
SonarQube
Source: the SonarCloud check run and bot comment on 86b268f. Quality Gate passed: 0 new issues, 0 accepted, 0 security hotspots, 0.0% duplication. Nothing is needed to pass.
CI
Every check is green on 86b268f. Firefox E2E passes 230/230, so spec 15 has now actually run in Firefox. In round 1 it was skipped behind the spec-13 flake that #1425 fixed.
New findings
None. The fix commit's changes are small and well targeted. The new watchdog is armed on every join path, including the reconnect rejoin, and is cleared on sync and on teardown.
Strengths
- The fixes follow the store's existing patterns: the join watchdog mirrors the save watchdog, and the failed-sync unwind mirrors
collabError. - The regression tests target the exact failure modes, e.g.
sentOps()equals['STOP_SCRIPT_EDIT']and a failed sync lands on Retry, and they drive the real handlers instead of_teardown(). - The #1419 tripwire is clearly labelled, with instructions for inverting it once #1419 is fixed.
🤖 Generated with Claude Code



Summary
Step 2 of Phase 3 of the collaborative script editing rebuild (issue #889; Step 1 is merged as
42d7844). Read-only lifecycle: withcollaborative_script_editingon, an editor opens the shared draft and browses it. Editing itself arrives in Step 3. The classicScriptEditor.vueis untouched — a newCollabScriptEditor.vueis chosen instead by a store getter, so nothing changes for anyone while the setting stays off (default, and still hidden from the UI).Design/rationale:
plans/COLLABORATIVE_EDITING_V3_PLAN.md(gitignored — ask if you want it pasted).What's new
ConfigScript.vuerendersCollabScriptEditor.vuewhensystemStore.isCollabScriptEditingis true,ScriptEditor.vueotherwise.editorslist containing our own id (the server commitsis_editorbefore broadcastingGET_SCRIPT_CONFIG_STATUS, so this is also the trigger on mount for a client that's already an editor).ROOM_CLOSED) gets an alert with Retry instead of a dead end.requestEditFailurenow shows the server's actual reason instead of a fixed string.draftAdapter, rendered with the existingScriptLineViewer(read-only), keyed by_uid— never index, never_id— so a save's id-patch or another editor's insert never remounts a row. Non-editors keep reading over REST exactly as today.REQUEST_SCRIPT_CUTScutover is Step 5).useDraftPagescomposable: page numbers, max page, a page's lines, and the previous line across a page boundary.Found while writing this step's E2E — not a regression, filed separately
Reloading the page while editing does not currently keep
is_editorserver-side. Traced to shared WS session-lifecycle code (REFRESH_CLIENTreads the wrongSessionrow;on_closedeletes the old row, usually before the new connection can resume it) that predates this phase and had zero test coverage anywhere — genuinely never exercised before this step's spec reloaded mid-edit. No draft content is lost (the room rebuilds from its checkpoint on the next join); the user just sees "Edit" again and needs to click it to resume. Filed as #1419 and background-investigated: confirmed empirically via two real-WebSocket integration tests, which also found a second, independent race the initial write-up hadn't identified. Correctly left unfixed pending a real design decision (a resumable-trace mechanism, not a one-line patch) rather than a guessed change.CollabScriptEditor.vue's reload-rejoin code path is already written for the fixed behaviour and needs no client change once #1419 lands.Also fixed two pre-existing E2E locator collisions surfaced by the new spec (not new duplication):
button:has-text("Edit")also matches the Stage Direction Styles sub-tab's own Edit button;#page-inputisn't unique app-wide sinceApp.vuemounts its own hidden global "Go to Page" modal on every route. Both now scope to the element that's actually open/visible, matching the pattern spec10already uses for the classic editor's identical modal.Test plan
client-v3: 231 Vitest (+22 for this step — composable, store, and a component test suite that includes a mutation check confirming an index-based key would have failed the_uidtest),tsc --noEmit,eslint,prettier,npm run build: clean.dev(which includes the Electron removal and the Flaky Firefox E2E: 03-system-config 'can configure RBAC permissions for testuser' fails first attempt #1412 Firefox-flake fix): 229/229 in both.01–14(setting off) unaffected.Notes for reviewers
requestEditFailure's new optionalreasonparam is a compatible change to a shared action; harmless for the legacy editor, which doesn't pass one.🤖 Generated with Claude Code
https://claude.ai/code/session_01TjTnfchFvvQvKZUZv2jpT8