Wire the open-time writability probe and read-only lock
Closes the gap found at m10: enterUnwritableLock existed with zero call
sites. WritabilityProbe classifies the cause volume-first - a board on a
read-only DMG is also permission-denied by access(2), and "you don't
have permission" would send the user to a Get Info panel that cannot
help - with a pure classify(volumeIsReadOnly:isWritable:) truth table
and a two-syscall probe that rebuilds its URL to defeat NSURL resource
caching. ReadOnlyLockReason.unwritableLocation now carries the cause;
BannerCenter phrases the two ("this board's volume is read-only" vs
"you don't have permission to change this folder").
The probe wires once in BoardStoreRegistry.acquire, immediately after
the store loads - every open path funnels through it, and running
before the loose-file relocation and agent-guide hooks makes the
skipped-with-log guide write true by construction (its isWritableFile
pre-check demotes to second line of defense). The board still opens:
lock, not refusal.
The reconciling re-probe is now symmetric per 02's settled text - a
volume gone read-only mid-session raises the lock at the next probe
(sibling locks settle first, so a root returning read-only lands the
honest lock); the stale "deliberately one-way" comment and its pinning
test are gone. Save as Template's carve-out predicate extracted to a
testable allowsSave (behavior unchanged); Duplicate stays disabled.
11 tests added. 1649 green on both schemes.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -421,7 +421,7 @@ struct BoardAnnouncerSpeechTests {
|
||||
@Test("A lock whose cause changed is news again")
|
||||
func changedLockCauseSpeaksAgain() {
|
||||
var facts = BoardAnnouncer.ReloadFacts()
|
||||
facts.lockBefore = .unwritableLocation
|
||||
facts.lockBefore = .unwritableLocation(.permissionDenied)
|
||||
facts.lockAfter = .vanishedRoot
|
||||
|
||||
#expect(
|
||||
|
||||
Reference in New Issue
Block a user