Denial is not absence — detection learns the unverifiable answer 06 ruled for it

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-06 17:53:23 -04:00
parent 503ec4872c
commit 26239200ea
10 changed files with 464 additions and 58 deletions
+150 -1
View File
@@ -9,7 +9,9 @@ import Testing
/// The rule has four claims and this file is one test per claim: root wins, an ancestor is nested,
/// neither is `none`, and the answer is re-derived rather than remembered "a board can therefore
/// change mode between opens (e.g. the user ran `git init` in a terminal) the app just reflects
/// what it finds."
/// what it finds." `BoardGitEntryProbeTests` and `BoardGitModeDenialTests` below pin the
/// 2026-08-06 axis on top of it: "Denial is not absence" a check the sandbox refuses must read as
/// `.unverifiable`, never as `.none`.
// MARK: - Fixtures
@@ -127,3 +129,150 @@ struct BoardGitModeTests {
#expect(BoardGitMode.detect(boardRoot: fixture.root) == .none)
}
}
// MARK: - Entry probe classification
/// **The errno-aware probe underneath detection** (06-history-undo.md Rules Detection, "Denial
/// is not absence", ruled 2026-07-31) pinned directly, one test per classification, before the
/// walk that builds on it is asked to prove anything.
///
/// The `denied` cases chmod a real directory to `0o000` tests run unprivileged, so `EACCES` is
/// genuinely reachable this way and restore it with an explicit `defer` declared *after* the
/// fixture's own teardown defer, so it runs first (Swift's LIFO defer order):
/// `BoardDuplicatorTests.aFailedWalkRemovesThePartialSibling` is the precedent this mirrors, so a
/// failed assertion can never leave an unremovable temp directory behind.
@Suite("Board git mode ▸ entry probe")
struct BoardGitEntryProbeTests {
@Test("A `.git` directory probes as exists")
func probesExists() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try makeGitDirectory(at: fixture.root)
#expect(BoardGitMode.probeGitEntry(at: fixture.root) == .exists)
#expect(BoardGitMode.hasGitEntry(at: fixture.root), "the boolean convenience agrees")
}
@Test("A plain folder with no `.git` probes as absent")
func probesAbsent() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
#expect(BoardGitMode.probeGitEntry(at: fixture.root) == .absent)
#expect(!BoardGitMode.hasGitEntry(at: fixture.root))
}
@Test("A folder somewhere the sandbox denies traversal probes as denied, not absent")
func probesDenied() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let outer = fixture.root.appendingPathComponent("outer", isDirectory: true)
let inner = outer.appendingPathComponent("inner", isDirectory: true)
try FileManager.default.createDirectory(at: inner, withIntermediateDirectories: true)
// Chmod the *parent*, not the probed folder itself: resolving `inner/.git` needs search
// permission on `outer`, which a plain unix permission bit can deny for the test's own
// unprivileged user exactly as the sandbox denies an ungranted ancestor.
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: outer.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: outer.path) }
#expect(BoardGitMode.probeGitEntry(at: inner) == .denied)
#expect(!BoardGitMode.hasGitEntry(at: inner), "the boolean convenience collapses denied to false, like absent")
}
}
// MARK: - Denial-aware detection
/// **The walk semantics denial adds** (06 Rules Detection): a denied ancestor never ends the
/// walk early, because a farther ancestor's `.git` still makes repo-nested certain; only a walk that
/// finds nothing at all *and* saw a denial along the way reads `.unverifiable`.
@Suite("Board git mode ▸ denial-aware detection")
struct BoardGitModeDenialTests {
@Test("A denied board-root probe is unverifiable outright — the walk never runs")
func deniedRootProbeIsUnverifiable() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let boardRoot = fixture.root.appendingPathComponent("board", isDirectory: true)
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: boardRoot.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: boardRoot.path) }
#expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
}
@Test("A denied ancestor with nothing found anywhere else reads unverifiable")
func deniedAncestorWithNothingFoundIsUnverifiable() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
let blocked = fixture.root.appendingPathComponent("blocked", isDirectory: true)
let boardRoot = blocked.appendingPathComponent("board", isDirectory: true)
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: blocked.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: blocked.path) }
let walk = BoardGitMode.ancestorWalk(above: boardRoot)
#expect(walk.root == nil)
#expect(walk.sawDenial)
#expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
}
@Test("A denied nearer ancestor never hides a `.git` on a farther one — repo-nested is certain")
func deniedAncestorWithARepositoryFartherUpIsRepoNested() throws {
// **A note on what chmod can and cannot simulate**: the sandbox denies a *specific path*
// independently of the filesystem's own permission bits an ancestor above the board's
// grant can be denied while the board root itself, inside the grant, stays fully readable.
// POSIX `chmod`, in contrast, cascades: removing search permission from a real ancestor
// directory denies resolving *everything* beneath it, board root included, which is a
// strictly stronger (and still individually honest) denial than the sandbox's. So this test
// proves the walk's own claim directly `ancestorWalk(above:)` never touches `boardRoot`
// itself, only the candidates above it, and is unaffected by that cascade.
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try makeGitDirectory(at: fixture.root)
let blocked = fixture.root.appendingPathComponent("blocked", isDirectory: true)
let boardRoot = blocked.appendingPathComponent("board", isDirectory: true)
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try FileManager.default.setAttributes([.posixPermissions: 0o000], ofItemAtPath: blocked.path)
defer { try? FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: blocked.path) }
// "A farther ancestor showing `.git` makes repo-nested certain regardless of the denied
// nearer one nearest-wins only affects which root you'd name, not whether one exists."
let walk = BoardGitMode.ancestorWalk(above: boardRoot)
#expect(walk.root?.standardizedFileURL == fixture.root.standardizedFileURL)
#expect(walk.sawDenial, "the denial is still recorded, even though it didn't decide the outcome")
// `detect(boardRoot:)` itself reads `.unverifiable` here not `.repoNested` but for the
// cascade reason above, not because the walk's certainty claim is false: `blocked` sits
// between the filesystem root and `boardRoot`, so chmoding it also denies **`boardRoot`'s
// own** `.git` probe, and `detect` answers that denial before the ancestor walk ever runs
// (06 Rules: "probe the board root's `.git` first denied `.unverifiable`"). A real
// sandboxed board, whose own root sits inside the grant, would not hit this path its own
// probe would succeed and the walk above is what would then run and find `.repoNested`.
#expect(BoardGitMode.detect(boardRoot: boardRoot) == .unverifiable)
}
@Test("All-clean paths are unaffected: none, git, and repo-nested still read as before")
func cleanPathsAreUnaffected() throws {
let plain = try WriterFixture()
defer { plain.tearDown() }
try plain.item("", Item.board)
#expect(BoardGitMode.detect(boardRoot: plain.root) == .none)
let gitBoard = try WriterFixture()
defer { gitBoard.tearDown() }
try makeGitDirectory(at: gitBoard.root)
#expect(BoardGitMode.detect(boardRoot: gitBoard.root) == .git)
let nested = try WriterFixture()
defer { nested.tearDown() }
try makeGitDirectory(at: nested.root)
let board = try makeSubfolder(nested, named: "project/docs/board")
#expect(BoardGitMode.detect(boardRoot: board) == .repoNested)
}
}