fix(diagnostics): deep-review round 4 — a claim only its owner may write, and a player that keeps the Tab
Round 4 was targeted at the surfaces rounds 2 and 3 changed. 29 findings raised, 26 survived deduplication, 12 survived three adversarial readings each. `set_delivery` renewed the claim but never checked it was still ours, while its sibling `release_send` always did. `expire_on_commit` is off, so the token it re-stamped came from the caller's own memory, not the row: a send that outlived its 600 s lease came back, overwrote the take-over send's committed links with a full-column write, and put its own dead token back. It now takes the token, reads the row's claim with a targeted scalar (not a refresh, which would discard the `issue_url` assigned one line earlier) and raises `SendClaimLost` rather than writing. An issue created by a send that had already lost its claim is logged with its number — that record is the only one that will exist. The tracker's settings are read above the pre-call checkpoint instead of inside `_client`. Reading them there was a SELECT one line after the commit that was meant to end the transaction, and held it — idle, snapshot pinned — across the repository check and the create. The comment and the module docstring said otherwise; measuring it in pg_stat_activity is what settled it. Two more comments were saying things the code had stopped doing: the `**state` spread does not carry the claim (`set_delivery` strips and re-stamps it), and react-query does not debounce the preview — the editor being unmounted is what paces it. The composer stopped stranding the focus on `<body>`: a send that creates the issue and then fails on an attachment unmounts the button that was just pressed, and only the success path handed the focus on. Export no longer saves into a refusal it does not need — the zip is the way out of a stuck send, and its pre-save was the one step that could not survive one. The delivery banner announces through a role instead of an `aria-live` created with its own message. `historyState` returns a union, so `chipTone` matching on the same keys is checked by tsc rather than by hope. PlexPlayer was the only full-screen overlay in the tree with no focus containment at all — Tab walked out of an opaque player into a page nobody can see. It now carries the shared trap. Closing that hole exposed the one underneath it: `useDismiss` registered as a layer that traps the Tab while installing no trap, so the player stood its own down for its own menus. Measured in a real browser: 30 Tabs with the settings panel open, 9 escapes into the nav rail, and 0 after. Escape ordering is unchanged — the panel first, then the player. Three tests that did not hold their line: the advisory lock's call site (deleting it left the suite green), the trail assertion whose `all()` was vacuous over an empty filter, and a `layerStack` assertion the stand-in dialog already forced. All eleven executable fixes mutation-checked red for the right reason; the two comment corrections have nothing to pin.
This commit is contained in:
@@ -90,8 +90,11 @@ describe("owning the keyboard and owning the Tab trap", () => {
|
||||
const prompt = pushLayer({ trapsTab: false });
|
||||
opened.push(prompt);
|
||||
|
||||
expect(anyBlockingLayerAbove(dialog)).toBe(false); // the dialog keeps trapping Tab...
|
||||
// ...and the page's own keys are quiet by the PROMPT's doing, not the stand-in dialog's: the
|
||||
// dialog is non-passive too, so asserting this with it still on the stack asserted nothing.
|
||||
removeLayer(dialog);
|
||||
expect(anyBlockingLayer()).toBe(true); // `j`, `g h` and the rest stay off the page behind
|
||||
expect(anyBlockingLayerAbove(dialog)).toBe(false); // ...and the dialog keeps trapping Tab
|
||||
});
|
||||
|
||||
it("a real dialog above does take the trap from the one below", () => {
|
||||
|
||||
@@ -100,7 +100,10 @@ export const qk = {
|
||||
debugSession: (id: string) => ["debug-sessions", id] as const,
|
||||
debugSessionEvents: (id: string) => ["debug-sessions", id, "events"] as const,
|
||||
debugScreenshots: (id: string) => ["debug-sessions", id, "screenshots"] as const,
|
||||
// The tracker's rendering of a body: the text is the key (typing pauses, then it renders again).
|
||||
// The tracker's rendering of a body: the text is the key. Nothing debounces it — react-query has
|
||||
// no such thing — so one fetch per distinct text. What holds the count down is that the composer
|
||||
// unmounts the editor while the preview is up, plus the cache for an unchanged draft; a preview
|
||||
// shown BESIDE the fields would need a real debounce first (see routes/debug_sessions.py).
|
||||
debugSessionPreview: (id: string, text: string) =>
|
||||
["debug-sessions", id, "preview", text] as const,
|
||||
adminDownloads: () => ["admin-downloads"] as const,
|
||||
|
||||
@@ -15,8 +15,14 @@ export function useDismiss(
|
||||
onClose: () => void,
|
||||
refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[],
|
||||
): void {
|
||||
// Registration + topmost-only Escape.
|
||||
useLayer(active, onClose);
|
||||
// Registration + topmost-only Escape. `trapsTab: false`, because this hook installs NO Tab trap:
|
||||
// it owns Escape and the outside-press, nothing more. Claiming the trap made a dialog underneath
|
||||
// stand its own down (`anyBlockingLayerAbove`) for a layer that traps nothing, so Tab walked out
|
||||
// of the dialog while one of its own menus was open — measured in a real browser on the Plex
|
||||
// player, whose settings and track menus are both dismissables (deep-review round 4). Every
|
||||
// consumer renders its panel INSIDE the surface below it, so that surface's trap already spans
|
||||
// the panel; a portaled one would need its own trap and must not use this default.
|
||||
useLayer(active, onClose, { trapsTab: false });
|
||||
|
||||
// Outside-press stays per-instance (the refs/positioning are the caller's): close on a mousedown
|
||||
// that lands outside every registered element.
|
||||
|
||||
Reference in New Issue
Block a user