feat(system): report progress while the session Trash empties - #5195
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS Job-based deferral with a locked consent-time snapshot, honest skip codes, and the security-critical descriptor walk correctly split out — sound, proportionate shape. [DESIGN-REVIEWED] 9ed7ee4 |
GPT 5.6 Review — ✅ human override acceptedHuman judgment by @chenmingwei23 overrides the GPT 5.6 finding for This comment is updated in place on each push. The model was not re-run because an authorized human decision supersedes it. False positive or not applicable? A repository writer can comment: |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of First-Principles-Verdict: PASS A reported defect ("I can't tell whether the irreversible delete happened") traced through every item; the two deeper causes are named, split out, and recorded in the spec. What this change shipsIntent: let a user see that emptying the session Trash is running, what it freed, and why anything was kept — a FIX for a verbatim-quoted report, plus the surface needed to say it.
Checks run: no existing job/status mechanism to reuse (each backgrounded handler in [FIRST-PRINCIPLES-REVIEWED] 9ed7ee4 |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS The refusal path says "this batch" but never says which — with several batches listed, the one message users must act on points at nothing. Watch
Suggestions
[UX-REVIEWED] 9ed7ee4 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsBoth candidates fail the falsification bar, and I found no grounded defect to add. Candidate 1 (missing SEL audit on the fail-closed Candidate 2 (unsynchronized Step 2 turned up nothing new: the fail-closed path sets No findings. [OPUS-REVIEWED] 9ed7ee4 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
fc2b427 to
772e0ed
Compare
|
All findings from GPT 5.6, Opus 4.8, First Principles and UX are addressed at 772e0ed. Disposition, one by one: GPT BLOCKING - Windows junction traversal. Real, and fixed by removing the traversal entirely rather than hardening it. GPT / UX - a kept batch reported success. Real. GPT / Opus - the slot claim raced the totals read. Real. UX - the last-finished job rendered forever. Real. A settled job stops being reported 10 minutes after it finishes ( UX - the failure copy dropped what it had freed. Fixed: UX - First Principles - fields with no consumers. Agreed and applied: One consequence worth flagging: the 409 body now nests the job under |
772e0ed to
0e44e66
Compare
|
Round 2 findings addressed at 0e44e66. GPT BLOCKING - per-file deletion has a symlink-swap race. Real, and the diagnosis is right: validating a path and then unlinking it resolves the prefix twice, so a component swapped to a link in between is followed. The suggested fix was to revert to batch-level Two tests, one for the mechanism and one for the outcome: Windows has neither GPT BLOCKING - a denominator failure permanently wedges emptying. Real. The staged total is read through First Principles - Also fixed, from the Korean and Hindi style gates rather than a reviewer: three ko strings put a single particle after an interpolation, where the form depends on a final consonant that does not exist until render (now written with both forms), and one hi string used the formal second-person pronoun against style/hi.md section 4. |
0e44e66 to
49a15e2
Compare
|
Round 3 findings addressed at 49a15e2. GPT BLOCKING - the coarse path stats unvalidated manifest paths. Correct. The descriptor path validated each entry's components inline, and the size read on the rmtree path did not - so a tampered entry could make it measure, and report as freed, a file the delete never touched. The check is now one function, GPT BLOCKING - job status exposes unredacted content. Fixed: the refusal text now goes through the module's own GPT FINDING - polling stops at Three more garbled tokens in my translations. All mine, all real: ja |
49a15e2 to
e24f5fc
Compare
|
First Principles items addressed at d69d8ff. GPT, Design, UX and Opus are clean/PASS on the descriptor-only head. Subtraction:
The WebSocket broadcast, though: not in this PR. The observation is right - the dashboard's freshness mechanism is WS invalidation, and one broadcast when a storage mutation settles would let this query go back to being cache-backed and delete the per-job settled-refresh effect too. I have recorded that as the better long-term shape in the query's own comment rather than doing it here, for two reasons. First, cost and timing: it adds a backend broadcast surface to a PR that has already taken eleven rounds of blocking review, and every recent round has been a case of me moving a mechanism and leaving the layer beneath it inconsistent. Adding a new notification path is exactly that shape again. Second, and more substantively, an event and a mount-time read are not interchangeable for this defect. A missed event brings the wrong list straight back: a tab that stays open across a gateway restart, or a reconnect that drops the frame, has a warm cache and no invalidation, which is the reported bug verbatim. The mount-time read is the floor that holds when the event does not arrive; the event is the optimization that removes the scan in the common case. The right end state is both, with the read as the fallback - not the event instead of the read. So the scan cost stands, bounded: one extra store walk per time the screen is opened, on a screen someone opens to decide what to delete. |
|
Round 12 finding addressed at aa7036f. GPT BLOCKING - premature manifest deletion hides recoverable files. Correct, and caused by my own previous round: the descriptor sweep refuses to remove a regular file it does not recognise, the manifest is such a file, so I unlinked it BEFORE the sweep to keep the batch removable. That created the window GPT names - a listed file whose unlink failed leaves the batch on disk with no manifest, and The manifest now goes LAST: its size is measured up front, the sweep is told to skip it by name (
Writing that test surfaced a second bug, mine and worse in kind: my first version called |
|
Round 13 finding addressed at 3bb3564. This is the 7th finding inside the delete function, and the SECOND consecutive one about premature manifest deletion - same defect, different route. GPT BLOCKING - a tampered manifest can delete its own recovery metadata. Correct. Last round I kept the manifest out of the SWEEP and removed it only after the sweep proved the batch empty. But an entry reading The loop now refuses an entry that names the manifest, and the manifest is removed in exactly one place: after the sweep reports the batch otherwise empty.
Worth stating plainly rather than presenting this as a clean catch: two rounds in a row have been the same invariant ("the manifest is the batch's recoverability, so it dies last and only once") enforced at one site and missed at the adjacent one. The invariant is now stated in the code at both sites and in the module spec. If a third route into it appears I will stop and hand this function to a human reviewer rather than patch a third time. |
|
First Principles items addressed at 7b056a3. GPT, Design and UX are all clean/PASS on the previous head, and Opus reported no blocking findings. Subtraction:
Staying deferred for the reason I gave earlier: an event and a mount-time read are not interchangeable for the reported defect. A missed event brings the stale list straight back - a tab open across a gateway restart, or a reconnect that drops the frame, has a warm cache and no invalidation, which is the bug verbatim. The right end state is the push event PLUS the read as the floor, and that is a change to the dashboard's freshness mechanism rather than to this screen, so it belongs in its own PR with those four call sites converted together. |
|
UX and First Principles items addressed at c263ccc. GPT, Design and Opus are clean on the previous head. UX - a stale Gated on the ABSENCE of a job rather than on the mutation state: UX suggestion - "Nothing was removed." is false on the 409 path. Taken: another delete IS removing things there, it just was not started by this request. Reworded to "This request did not start a delete." in all 12 catalogs, which is true in both cases. First Principles - First Principles - |
|
Round 14 finding addressed at f1e9b50. Not in the delete function - this one is my test fixture. GPT BLOCKING - teardown leaves a filesystem worker running. Correct, and worse than the hang it replaced. My Teardown now WAITS for a still-running job, bounded at 30s, and on timeout fails with the test's name rather than walking away from a live thread. A hang is a worse test experience than a cancel; a thread deleting files past teardown is a worse defect, and the bounded wait avoids both.
Standing count for the escalation rule I agreed with the user: the delete function's consecutive streak is broken - rounds 13 and 14 were the frontend and this fixture. |
|
Round 15 addressed at cb11c4c. Both items are real; neither is in the delete function. BLOCKING - a Windows drive-relative name bypasses containment. Reachable, verified before fixing: Fixed as you suggested - any One consequence worth stating rather than burying: FINDING - a locked Windows file is reported as freed. Also correct. Escalation count: the delete function's consecutive streak stays broken - rounds 13, 14 and 15 were the frontend, the test fixture, and the shared name validator plus the Windows branch. |
|
Design and UX round addressed at b7d8302. GPT and Opus are clean, First Principles is PASS. UX - UX - UX - Design - the delete rewrite is never weighed in the description. That is the actionable half of the concern and it is fixed: the body now has a section stating the Design - Design suggestion - extract the job/poll pair into |
|
Round 16, and the 8th finding inside the delete function. Answering this one rather than fixing it, with evidence, and flagging it for a human decision instead of a 9th patch. BLOCKING - final-component races bypass the unlisted-file guard. The race is real and I am not disputing it. Between the check that accepts an entry and The proposed fix does not remove it, and makes the named harm more likely. That is the same check-then-unlink-by-name on the final component, so the identical race exists there. The difference is what happens when it fires: What the residual risk actually requires. An actor able to rename entries inside a batch directory under the user's own data home is running with write access to that tree - the same access needed to read or delete the sessions directly, without waiting for an empty job. So the guard this race bypasses protects against unaccounted files appearing by accident (a partial move, a crash, a stray editor file), which is what I am not posting an override and I am not patching this a 9th time. Two rounds ago the design review said a human should consciously accept the descriptor-rewrite trade; this finding is the same question sharpened - whether an unclosable final-component race is acceptable given the guard buys real protection against accidents and the alternative gives up the guard without closing the race. That is the repo owner's call, and it is now in front of him. If he prefers the revert, the right shape is splitting this PR: the progress fix on |
|
Rebased onto main at 64c3ad9 - #5191 landing put this branch in CONFLICTING, which no amount of review progress can clear. Three conflicts, all resolved, no behaviour change: both PRs' test blocks kept in SessionStorageScreen.test.tsx (50 pass, main's 7 pagination tests plus this PR's), the React import kept GPT and Opus are clean on this head; UX is PASS. First Principles - description/diff drift. Correct and fixed: item 5 claimed First Principles + Design - the security rewrite riding in a progress fix. Both lanes land on the same point from different directions, both say it is not blockable, and both point at the sentence I put in the description offering the split. Nothing for me to change: the offer is real, it is the one decision in this PR, and it belongs to the repo owner rather than to me. It is in front of him now. If he takes the split, the shape is the progress fix on First Principles - Not carried over from the last round and still open for the owner: Design's point that |
|
Round 17 addressed at 330da39. New finding, not the one I answered last round and not in the delete function - this is the shared name validator. BLOCKING - an embedded NUL can abort an irreversible delete mid-batch. Real, and I verified all three legs before touching anything: Fixed in two places, and they are not the same kind of fix:
Being exact about what is verified, because the two halves are not equally covered. Also in this push: the PR body's item 5 no longer claims |
|
UX round addressed at 934cbe2. GPT and Opus are clean on the previous head. Missing diacritics in the three late-added keys. Real and mine. While fixing them I found one you did not name, same class one layer deeper: the Spanish copy said "Vuelve a intentar Eliminar definitivamente" while the actual button in
Suggestion - link "the gateway log" to the Logs page. Agreed it is the better end state and not doing it in this PR. Turning those two sentences into sentence-plus-link means interpolating a component across 12 locales for both Verification: 162 frontend tests pass (screen + i18n style + key reference), source-strings 0 findings, changed-values 0 catalog QA findings, tsc clean. |
|
Round 18 addressed at 381487d. Fixed, not answered - and worth saying why this one got a patch when the round-16 race got an argument, since they look like the same shape. BLOCKING - the target snapshot does not preserve batch identity. Correct. Why this one is closeable and the file-level race is not: everything after the batch is opened addresses it through The snapshot now records
New skip code rather than reusing Verification: 178 backend tests, 162 frontend, mypy and flake8 clean on the changed files, black gate passed, source-strings and changed-values 0 findings, tsc clean. |
|
Round 19. Not fixing this one, and not overriding it either - stopping here and handing the decision to the repo owner, which is what I said I would do if this came back. This is the round-16 finding again. The POSIX half - Last round I closed the BATCH-level version of this by carrying The Windows observation is new and I am not going to pretend otherwise. That leaves the real question, which is the one Design and First Principles have both already put in front of a human this round: the hardened delete is POSIX-only, and whether that asymmetry is acceptable - or whether this PR should be split into the progress fix on Standing count, for whoever picks this up: 19 review rounds, 9 findings touching the delete path, 2 of them the same race stated twice. Everything else raised on this PR is fixed and dispositioned; the last head carries GPT-clean, Opus-clean, UX PASS results from earlier rounds and this is the only open item. No |
|
Split, per the repo owner's decision. Head is now df00da4. This PR keeps the stdlib Kept, because they are the progress feature rather than the security change: the 202+job with a counters-only status read, per-batch progress against a staged total, The removed work is preserved, not discarded. Branch To be straight about what the split costs: progress is now one report per batch, and on the reported 55,323-session batch that is a single step from nothing to done - which is most of the granularity the original complaint was about. What survives is that the run is now visible at all (a job, a running state, a byte figure, and a stated outcome instead of silence), and that a batch which was NOT deleted says so instead of reading as success. Per-file granularity comes back with #5430. Also worth recording against my own earlier position: I argued for keeping the two halves together, and three reviewers independently reached the same conclusion the other way - Design ("a human should consciously accept that trade"), First Principles ("take that offer seriously; it is the one real decision here"), and GPT prescribing the revert three times. The convergence was the signal, not any single lane. Verification on the reduced branch: 169 backend tests pass ( |
|
Answering this one with a measurement rather than re-adding the removed code, and not overriding it either. The window this describes already exists on main, with the same per-batch duration. Compared the two directly. Main's This branch is byte-identical in that structure. So on main today, batch N is resolved BY NAME after batches 1..N-1 have been deleted - on the reported 55,323-session store that is minutes after the click. A rename onto an approved name wins there exactly as described here. What this PR actually adds to that window is the gap between the POST's snapshot and the worker task starting. That task is created with And the snapshot makes one thing strictly safer, not less. Main re-enumerates for "empty everything" when the delete RUNS, so a batch staged after the user clicked would be destroyed. Closing the name-resolution window needs object-identity binding, which needs fd-based removal. That was in this PR until an hour ago; it is now #5430 with the design, the tests already written, and the branch On the prescribed remedy: "revert the asynchronous name handoff" removes the reported defect's fix - the whole point is that the delete outlives the request so a user who navigates away is not silently abandoning a half-done irreversible operation - while leaving main's identical per-batch window in place. That trade does not hold up. To be explicit about what ships unfixed: between the snapshot and each batch's removal, a process with write access to the trash root can retarget an approved name, and this PR does not close that. It is pre-existing, unchanged in shape, and tracked in #5430. No override posted. |
|
Rebased onto main at 8c1ff41. Zero code edit - the three red Backend Tests shards were a stale base, not a defect.
Worth noting the guard did exactly its job here: it is the byte-equality check added in #5164, and it caught a real drift between a branch and the schema rather than a code defect. Nothing to fix on this side. One local-tooling note in case anyone hits it: Verification after the rebase: 169 tests in the two session-storage files, 10 in the config-baseline file, 162 frontend, mypy and flake8 clean on the changed files, black gate passed, source-strings and changed-values 0 findings, tsc clean. The GPT finding on the previous head is answered at #5195 (comment) - the name-resolution window it describes is present on main today with the same per-batch duration, and closing it needs the fd-based removal now tracked in #5430. |
Emptying a staged batch held the request open for the whole delete - minutes on a store of tens of thousands of sessions - and the screen only greyed three buttons for the duration. Nothing said it was running, how far it had got, or that it had finished, and because the inventory query never refetched on mount, leaving the page and coming back re-rendered the pre-delete list. Delete files individually with a progress callback, keeping the path-containment and unlisted-file refusals unchanged; answer 202 with a job and report it from a counters-only status read that no longer walks the store; land a refusal or an unexpected error on the job instead of losing it with the request. The screen polls that job, shows bytes freed against the staged total, says the run survives leaving the page, and re-reads the inventory once the job settles. Closes #5194
|
First Principles round addressed at 9ed7ee4. All five items were real leftovers from the split - dead code and stale prose I should have swept when I reduced the PR, plus one screenshot that had quietly become fiction. Design, UX, GPT and Opus are clean/PASS on the previous head. The mid-run screenshot depicted a state this code cannot produce. The sharpest of the five and the one I would have missed. The fixture staged ONE 18GB batch while the job reported 6.9GB freed - but progress is now per batch, so with a single batch the bar can only be 0% or 100%. The frame was fixture-fed fiction. The fixture now stages THREE batches (6.9GB + 7.1GB + 4.0GB = 18GB, sessions summing to the same 55,323) and 6.9GB is exactly "the first of three finished", which is a figure the shipped code genuinely produces. Comment in the harness says why the count matters, so nobody collapses it back to one. Recapturing surfaced something unrelated that I am NOT fixing here: a batch with Subtractions, all confirmed before removing:
Undeclared items - both now in section 3 of the description as item 6: the paused-controls banner ( Verification: 169 backend tests, flake8 and mypy clean on the changed file, black gate passed, eslint clean on the harness, three frames recaptured. |
|
This is the same finding as #5195 (comment), third wording. Not re-adding the removed code, not overriding, and not answering it a third time identically - one new piece of analysis, because the prescribed remedy changed and it deserves checking rather than dismissing. The new remedy does not close the window it names, and it costs two things. "Keep target validation and deletion in one locked call" means moving
So the remedy removes the millisecond part and leaves the minutes part, on a window that main already has in the same shape. What it costs is real: the 400 What would actually close it is binding the delete to the object rather than the name, which needs descriptor-based removal - removed from this PR two hours ago on the repo owner's decision and tracked in #5430. Re-adding it here would undo that decision. I have now answered this three times and I am not going to keep spending rounds on it. Handing it back: this PR cannot reach green while this lane holds, and the paths to green are an override, or landing #5430 first and rebasing this on top. Both are the owner's call. Everything else on the PR is clean - Design PASS, UX PASS, Opus no blocking, First Principles' five subtractions all applied. One idea worth recording for #5430 rather than doing here, since it came out of this analysis: renaming each approved batch to a delete-staging name atomically under the lock at request time would narrow the window to the POST handler itself, without descriptors. It is still name-based and so not airtight, but it is materially narrower than what either version does today, and it may be a cheaper first step than the full descriptor walk. |
|
/ai-review override gpt 9ed7ee4: The name-resolution window is already on main with the same per-batch duration, so this PR does not introduce it; the prescribed remedy removes only the millisecond POST-to-worker gap while costing the synchronous 400 and the progress denominator. Binding the delete to the object needs fd-based removal, deliberately split out to #5430 with its tests already written. Answered three times in the thread. |
Human judgment recorded@chenmingwei23 marked the gpt AI finding as false positive, not applicable, or explicitly accepted for
This decision applies only to this commit. A new push requires a new judgment. |
|
All checks green at 9ed7ee4: 66 passing, none failing, MERGEABLE. Design PASS, First Principles PASS, Opus no blocking, GPT recorded as human override accepted (target=gpt, actor=chenmingwei23) for the name-handoff finding answered three times above and tracked in #5430. UX's two Watch items on this head are real, they are NOT fixed, and I want them recorded rather than lost - along with why they are not fixed yet. Both were exposed by my own change one round earlier. When the fixture staged ONE batch, "Restore what you can from this batch" had an unambiguous antecedent and a single dedup'd sentence matched reality. Changing the fixture to three batches - which I did because per-batch progress cannot produce a partial figure with one batch - made the copy wrong in two ways at once: the section-level sentence now says "this batch" above three identical-looking rows with no way to tell which was kept, and The right fix is the one UX names: carry the batch id in Why it is not in this push: a new head VOIDS the override just recorded, so shipping these needs another one. That is the repo owner's call, not mine, and it is in front of him now. If he prefers to ship, these want a follow-up issue and should be fixed before the multi-batch refusal path is common - it is the only irreversible surface here, and "restore this batch" pointing at nothing in particular is the wrong instruction to leave on it. To be explicit about what ships if this merges as-is: a refusal is reported, but on a multi-batch trash it does not say WHICH batch was kept, and two batches kept for the same reason read as one. Neither loses data - the kept batches are all still listed and restorable - but the user has to open the gateway log to know which row to act on. |
1. What is the problem?
Emptying the session Trash said nothing while it ran and nothing when it finished.
POST /api/system/session-storage/emptyheld the request open until every staged file was gone. On a real store that is one batch of 55,323 sessions and 18GB, which is minutes of filesystem work, and for all of it the screen only greyed out Restore / Cancel / Delete forever. Reported verbatim: "I don't even know whether it deleted - all I know is that when I came back 10 seconds later it was gone."Three defects sat behind the same click:
staleTime: Infinityand this screen's inventory query did not refetch on mount, so returning mid-delete re-rendered the CACHED pre-delete list - batch still there, totals unchanged. That reads as "it did not work"._empty_trash_lockedcorrectly KEEPS a batch holding files no manifest lists, because those are the only copy - and it does not raise, it logs and moves on. The endpoint reported success regardless, the batch simply stayed, and no reason was given.shutil.rmtreeonce per batch, so the only granularity available was "a batch finished" - on a single 55,323-session batch, one step from nothing to done.2. Why this issue matters to the user
This is the only irreversible action in the storage surface and the only one that returns disk space. "Did the thing I cannot undo actually happen" is the question the screen exists to answer, and it was the one question it could not.
3. How our fix solves it
Chain from the symptom down:
Per-batch
rmtreeis the wrong granularity for a big batch, but fixing that properly means replacing the removal itself, which is a security change and is now split out (see below). What this PR does instead is make the one report per batch HONEST._delete_listed_filesmeasures the bytes from the manifest, callsshutil.rmtree, then measures again and subtracts what survived -ignore_errors=Truereturns quietly when a locked file leaves the batch standing, so the up-front figure would otherwise be reported as reclaimed with the bytes still on disk. It then checks as a POST-CONDITION that the batch is actually gone, and reports it as kept if not. The byte sums come from the manifest rather than a directory walk, which is also the safer way to get them: on Windows a junction is not a symlink (os.path.islinkreports False for one), soos.walkdescends into it and would measure files outside the trash entirely. Callers reach that function only after_unlisted_fileshas confirmed the batch holds nothing the manifest omits, so the manifest describes the whole batch.The request could not report anything because it only returned when the work was over. It now answers 202 with a job, and
GETon the same path returns that job's counters. The read touches no store - every other endpoint in this module walks the sessions on disk, which is why none of them can be polled and this one can. A finished job stops being reported after 10 minutes, so an outcome is never presented as current days later.Leaving was already safe but unstated. The job is deliberately not tied to the request, and the status read is what lets the screen pick up a run started before it mounted. The screen says so in words.
A refusal had nowhere to land once the request was answered.
empty_trashgained anon_skipcallback carrying a reason CODE per kept batch; the job records them, audits asrefused, and the screen states them in the user's language - a code rather than the gateway's own sentence precisely so it can be translated. A crash lands the same way as a generic line with the detail in the log; the broadexceptis deliberate, since an unhandled error would otherwise leave the job flagged running for the life of the process.The stale list is fixed by
staleTime: 0on the inventory query, plus one re-read when a job settles, keyed by job id so two empties each get their own refresh and a remount does not re-fire for one already accounted for.What "empty everything" MEANS changed, and a reviewer should know it. The POST now resolves, under the storage mutation lock, which batches it will destroy and hands the worker that explicit list, so the request's own moment is the consent boundary and a batch staged afterwards survives.
empty_trash(None)enumerated the trash inside the worker, which deferring the work moved from milliseconds after the click to minutes - and a staged batch is the only copy of those sessions. If that read fails forall: truethe job settles with a reason and deletes nothing; an explicit selection proceeds, because the caller already named the set.Progress is measured in bytes against the staged total, not in sessions: the delete walks files, a session is more than one file, and a session-shaped count would be a guess. The denominator comes from the staged manifests - the same figure the trash row already showed.
A second empty is refused with 409 rather than queued, and the slot is claimed in the SAME synchronous step as the check: reading the staged totals first put a suspension point between guard and claim, so two near-simultaneous POSTs both passed and the second overwrote the first. The job slot is process-local and not persisted: if the gateway dies mid-delete the files are gone either way.
Two files this PR already owns became black-clean and were pruned from
.github/black-baseline.txt(2 deletions); the formatting churn inside them is that graduation, not a change of behaviour.What this PR deliberately does NOT do
An earlier revision of this PR also replaced
shutil.rmtreewith a descriptor walk -every ancestor opened
O_NOFOLLOW, files named by the manifest and removed by(directory fd, name), directories removed bottom-up by descriptor - which closes aTOCTOU window that deferring the delete into a background job widens from milliseconds
to minutes, and which would also allow per-FILE progress instead of per-batch.
That work is now split into its own change, for reasons three reviewers reached
independently: it is a bespoke security-critical surface arriving inside a UX fix, it is
POSIX-only (Windows has neither
openatnorO_NOFOLLOW, so the hardened path would notexist exactly where junction semantics differ), and most of this PR's review rounds were
spent inside it. It deserves its own review on its own merits rather than being carried by
a progress bar.
So the delete here is the stdlib
rmtreethis repo already shipped, unchanged inmechanism. Everything this PR adds is reporting: a job, progress, measured byte figures,
and refusals the user is actually told about.
empty_not_startedcovers a POST that never became a job -onSettleddisarms the confirm on any outcome, so a failed start cleared the button and left nothing behind, which is this PR own reported symptom in miniature.Screenshots
Captured by
website/scripts/capture-trash-empty-progress.mjsagainst fixtures modelled on the reported store (one policy batch, 55,323 sessions, 18GB).Mid-run - partial figure, bar, and the line that makes leaving safe to do:
Finished - what it freed, with the batch gone from the list:
A batch KEPT - the refusal that raises nothing and used to render as "Freed 0 B." above a batch that was still there:
4. What tests we did
test/test_session_storage.py(98 in file, green): progress is a monotonic running total across batches whose last value equals the return; a kept batch reportsSKIP_UNLISTED_FILESand frees nothing; a linked directory inside a batch is not traversed and its target survives; a manifestrelpointing at another batch is refused. The 12 pre-existingempty_trashsafety tests pass unchanged, which is the evidence containment and the unlisted-file refusal did not move.test/test_session_storage_api.py(43 in file, green): 202-then-freed with the audit; a partial figure read MID-RUN while the delete is held open; two SIMULTANEOUS POSTs producing exactly one job (202 + 409); a kept batch audited asrefusedwithskippedon the job; a refusal and an unexpected error each finishing the job instead of hanging it; a stale finished job no longer reported.test/test_error_code_contract.pygreen: the 409 body carries a machine-readablecode.website/src/test/SessionStorageScreen.test.tsx(36 in file, green): running figure, the "keeps running" line, picking up a job started before mount, the finished total, the kept-batch refusal (and a repeated reason stated once), the partial-freed failure copy, other actions held, and the confirm disarming on acceptance.flake8,isort,mypy(1031 files) and the black gate all clean; coverage 89% / 91% on the two changed modules from these test modules alone (80% floor). Frontendtypecheck,lint(0 errors),i18n:checkall green with the pseudolocale regenerated.i18nAllLanguagesEntryred is gone.Backend Tests (shard 2)ontest_driver_session_directives.pyis inherited - it fails identically on fix(dashboard): add meta.mid tier to save-side foreign fold (#5152) #5196, whose diff is unrelated to both.5. Any other suggestions on the work
Closes #5194