Skip to content

Granting all-files access only moves the mirror root at the next cold start - #132

Merged
xroche merged 6 commits into
masterfrom
fix/storage-root
Aug 17, 2026
Merged

Granting all-files access only moves the mirror root at the next cold start#132
xroche merged 6 commits into
masterfrom
fix/storage-root

Conversation

@xroche

@xroche xroche commented Aug 17, 2026

Copy link
Copy Markdown
Owner

computeStorageTarget() adopted a new default root only when the current one was missing. But ensureExternalStorage() has already created it, so once projectPath was set that branch was dead for the rest of the process: granting all-files access left the running session writing to the old private root, and the move showed up at the next cold start, by which point the earlier mirrors sat outside the root being listed. The decision now sits in StoragePaths.resolveRoot, which takes no argument for the root in use and so cannot prefer it; that also fixes the legacy API<=29 grant callback, which recomputed through the same dead branch.

The same function erased a persisted BasePath as soon as it could not be vetted, so an unmounted volume or a revoked grant lost the setting for good. It is now kept and simply unused for that run. Every resume re-resolves, and the two things that must not repeat are gated on StoragePaths.rootMoved: initRootPath leaks a buffer per call by design, and the missing-directory warning is a toast.

The public-versus-private path choice moved to StoragePaths.defaultRoot too, so plain JUnit now drives what used to be asserted against the source text. A move's side effects run through StoragePaths.applyRootMove, and the tests record the calls rather than search for them: granting access moves the root, the first resolution counts as a move, and an unchanged root does not, however many resumes happen.

One surface stays uncovered, deliberately. The stub android.jar puts computeStorageTarget beyond a unit test's reach, so nothing asserts what the anonymous RootMoveActions at that call site does; a source-text check only confirms the call is there at all. Swapping the bodies of warnMissingDirectory and refreshProjectSuggestions leaves the whole suite green, the miswiring plain in the bytecode: a user whose base directory is missing would get no warning, and a toast for nothing once the root moved. Hardcoding missingDir to false, or inverting the loadedSuccessfully() guard, survive the same way. Catching any of it needs assertions on the source text, and the first rename breaks those.

Closes #128

xroche and others added 6 commits August 17, 2026 17:55
computeStorageTarget() adopted a new default root only when the current
one was missing, but ensureExternalStorage() has already created it, so
once projectPath was set the fallback branch was dead for the rest of the
process. Granting all-files access left the running session writing to
the old private root, and the switch surfaced only at the next cold
start, where the earlier mirrors no longer sat under the listed root.

The choice moves to StoragePaths.resolveRoot, which is blind to the root
in use by construction. The same function also erased a persisted
BasePath as soon as it could not be vetted; it is now kept and merely
unused for that run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Review of the previous commit found two consequences of recomputing on
every resume. initRootPath deliberately leaks a buffer per call, on the
assumption that re-init is rare, and the missing-directory warning is a
toast that would repeat for as long as the base stayed unusable. Both now
run only when the resolved root actually changes.

resolveRoot drops its baseIsDirectory parameter and derives the fact
itself: a boxed tri-state Boolean sitting next to a primitive boolean
transposes silently, and the caller was computing something the helper
already had.

The tests move from asserting on source text to driving the helper
against a real filesystem, which covers the cases a text match could not
tell apart. Verified by mutation: hardcoding the verdict, erasing under
the literal key, ungating the native re-init, and dropping the directory
check each fail exactly one test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The decision that re-points the root on a resume was only asserted as
source text, and the re-init assertion still passed with initRootPath
moved just outside the guarded block. rootMoved() and defaultRoot()
move into StoragePaths, so plain JUnit drives both the gate and the
public-versus-private path policy. What is still read from the source
is now brace-scoped, and the erasure check asks that the settings are
never edited rather than listing two spellings of remove().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
The gate test matched the literal head `if (StoragePaths.rootMoved(`, so a
mutant spelled `rootMoved(projectPath, projectPath)` kept the text intact,
never fired, and left the native log root permanently wrong. Inverting the
condition only died by accident, and lifting the body into a helper failed a
test that had nothing to say about behaviour.

The block becomes StoragePaths.applyRootMove, taking the two roots and a
RootMoveActions for the warning, the native re-init and the suggestion
refresh, so the test records which calls happened with which arguments.

While here: defaultRoot took `shared` first where its neighbour isWritable
takes it last, four same-typed File params one line apart. Both now read the
same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
… site

The recorder tests judge applyRootMove itself, so deleting its one caller
leaves them green while the native log root goes back to being wrong. A text
check cannot prove ordering or nesting, which is why the old block matcher
went; it can prove the call is spelled somewhere, which is all this asks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
Comments and Javadoc only, no behaviour change. The note on
getDefaultHTTrackPath had grown into a paragraph about private-directory
side effects, which is not what the line below it does. applyRootMove
documented two of its four parameters where the rest of the file
documents all of them, and the interface method it takes was the one
parameter in StoragePaths without final.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>
@xroche
xroche merged commit f930ea6 into master Aug 17, 2026
6 checks passed
@xroche
xroche deleted the fix/storage-root branch August 17, 2026 19:55
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.

Granting all-files access does not move the mirror root until the next cold start

1 participant