Align FolderWatcher with the newly ratified design decisions
The parallel design session resolved the m3 watcher findings; two changed behavior: .git internals are now filtered at any depth (a nested clone or submodule is a stray whose internals never render), and a root change landing mid-bracket is owned by the root-change path — endBracket() skips its mandatory reload once a .rootChanged tore the stream down, cleared when a fresh stream attaches. The skip is scoped by an explicit flag, not by stream absence: a watcher whose stream never came up still delivers post-bracket reloads, since distrust of FSEvents delivery is the mandatory reload's whole reason. Two new watcher tests; full suite 281 tests in 54 suites green. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
@@ -344,6 +344,34 @@ struct FolderWatcherTests {
|
||||
#expect(log.events == [.treeChanged(.foreign)])
|
||||
}
|
||||
|
||||
@Test("A repo nested inside a card folder is filtered like the root's own")
|
||||
func nestedGitChurnIsFiltered() async throws {
|
||||
let fixture = try WatchFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let cardFolder = "1e9a7c5e-0000-4000-8000-000000000000"
|
||||
fixture.makeDirectory("\(cardFolder)/.git")
|
||||
let log = EventLog()
|
||||
let watcher = FolderWatcher(root: fixture.root, debounce: testDebounce, latency: testLatency) {
|
||||
log.record($0)
|
||||
}
|
||||
#expect(watcher.start())
|
||||
defer { watcher.stop() }
|
||||
await drainStartupChurn(log)
|
||||
|
||||
// A clone living inside a card folder is a stray (01-storage-format.md) whose internals
|
||||
// never render — filtered at any depth, not just the board root's own repo (02, settled).
|
||||
fixture.write("\(cardFolder)/.git/index", "fake index")
|
||||
fixture.write("\(cardFolder)/.git/refs/heads/main", "deadbeef")
|
||||
await quiet(.milliseconds(700))
|
||||
#expect(log.events.isEmpty)
|
||||
|
||||
// The nested repo's *working files* are ordinary stray paths and still fire.
|
||||
fixture.write("\(cardFolder)/README.md")
|
||||
await waitUntil { log.count >= 1 }
|
||||
await quiet()
|
||||
#expect(log.events == [.treeChanged(.foreign)])
|
||||
}
|
||||
|
||||
// MARK: Root identity
|
||||
|
||||
@Test("Deleting the watched root delivers rootChanged and tears the stream down")
|
||||
@@ -367,6 +395,33 @@ struct FolderWatcherTests {
|
||||
#expect(!watcher.isWatching)
|
||||
}
|
||||
|
||||
@Test("A root vanish mid-bracket is owned by the root-change path; endBracket() is a no-op")
|
||||
func rootVanishMidBracketSkipsThePostBracketReload() async throws {
|
||||
let fixture = try WatchFixture()
|
||||
defer { fixture.tearDown() }
|
||||
let log = EventLog()
|
||||
let watcher = FolderWatcher(root: fixture.root, debounce: testDebounce, latency: testLatency) {
|
||||
log.record($0)
|
||||
}
|
||||
#expect(watcher.start())
|
||||
defer { watcher.stop() }
|
||||
await drainStartupChurn(log)
|
||||
|
||||
watcher.beginBracket()
|
||||
fixture.remove()
|
||||
// `.rootChanged` bypasses the bracket — the consumer must know *now*.
|
||||
await waitUntil(.seconds(10)) { log.events.contains(.rootChanged) }
|
||||
#expect(log.events == [.rootChanged])
|
||||
#expect(!watcher.isWatching)
|
||||
|
||||
// The bracket's mandatory final reload is skipped: the stream is gone, and the
|
||||
// root-change path owns recovery from here (02, settled) — `reattach(to:)` or the
|
||||
// vanished-root lock, each ending in a reload of its own.
|
||||
watcher.endBracket()
|
||||
await quiet(.milliseconds(700))
|
||||
#expect(log.events == [.rootChanged])
|
||||
}
|
||||
|
||||
// MARK: Stop
|
||||
|
||||
@Test("Nothing is delivered after stop()")
|
||||
|
||||
Reference in New Issue
Block a user