native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262import Foundationimport Testingimport XCTest
@testable import Prowl
@Suite(.serialized)@MainActorstruct GitWorktreeRegistryMonitorTests { @Test func observesGitWorktreeMoves() async throws { let layout = try RegistryLayout(entries: [:]) defer { layout.remove() } try layout.initializeGitWorktrees() let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) defer { monitor.cancel() }
try await events.expectEvent { try layout.git(["worktree", "move", "sparrow", "moved"]) } try await events.expectEvent { try layout.git(["worktree", "move", "moved", "moved-again"]) } }
@Test func observesMovesAfterAtomicGitdirReplacement() async throws { let layout = try RegistryLayout(entries: [:]) defer { layout.remove() } try layout.initializeGitWorktrees() let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) defer { monitor.cancel() }
try await events.expectEvent { let url = layout.gitdirURL(for: "sparrow") try Data(contentsOf: url).write(to: url, options: .atomic) } try await events.expectEvent { try layout.git(["worktree", "move", "sparrow", "moved"]) } try await events.expectEvent { try layout.git(["worktree", "move", "moved", "moved-again"]) } }
@Test func observesMovesAfterSameNameRecreation() async throws { let layout = try RegistryLayout(entries: [:]) defer { layout.remove() } try layout.initializeGitWorktrees() let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) defer { monitor.cancel() }
try await events.expectEvent { // Do not yield the main actor between removal and recreation. The other worktree // keeps the registry directory alive while the entry gets a new inode. try layout.git(["worktree", "remove", "sparrow"]) try layout.git(["worktree", "add", "sparrow", "sparrow"]) } try await events.expectEvent { try layout.git(["worktree", "move", "sparrow", "moved"]) } try await events.expectEvent { try layout.git(["worktree", "move", "moved", "moved-again"]) } }
@Test func promotesDirectoryWatchWhenGitdirAppears() async throws { let layout = try RegistryLayout(entries: ["sparrow": .directoryOnly]) defer { layout.remove() } let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) defer { monitor.cancel() }
try await events.expectEvent { try Data("first\n".utf8).write(to: layout.gitdirURL(for: "sparrow")) } try await events.expectEvent { try Data("second\n".utf8).write(to: layout.gitdirURL(for: "sparrow")) } }
@Test func observesNewEntriesAfterRegistryDirectoryRecreation() async throws { let layout = try RegistryLayout(entries: ["sparrow": .withGitdir]) defer { layout.remove() } let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) defer { monitor.cancel() }
try await events.expectEvent { // Removing the last worktree can remove the registry directory too. try FileManager.default.removeItem(at: layout.worktreesDirectoryURL) try FileManager.default.createDirectory(at: layout.entryURL(for: "sparrow"), withIntermediateDirectories: true) try Data("recreated\n".utf8).write(to: layout.gitdirURL(for: "sparrow")) } try await events.expectEvent { try FileManager.default.createDirectory(at: layout.entryURL(for: "swift"), withIntermediateDirectories: true) try Data("new-entry\n".utf8).write(to: layout.gitdirURL(for: "swift")) } try await events.expectEvent { try Data("moved\n".utf8).write(to: layout.gitdirURL(for: "swift")) } }
@Test func cancellationDoesNotRecreateEntryWatches() async throws { let layout = try RegistryLayout(entries: ["sparrow": .withGitdir]) defer { layout.remove() } let events = RegistryEvents() let monitor = try #require(GitWorktreeRegistryMonitor(repositoryRootURL: layout.rootURL, onEvent: events.record)) try Data("replacement\n".utf8).write(to: layout.gitdirURL(for: "sparrow"), options: .atomic) monitor.cancel() monitor.cancel() await events.drainEvents() try Data("after-cancel\n".utf8).write(to: layout.gitdirURL(for: "sparrow")) await events.drainEvents() #expect(events.count == 0) }
@Test func watchesGitdirFileOfEveryRegistryEntry() throws { let layout = try RegistryLayout(entries: ["sparrow": .withGitdir, "swift": .withGitdir]) defer { layout.remove() }
let urls = GitWorktreeRegistryMonitor.registryEntryURLs( inWorktreesDirectory: layout.worktreesDirectoryURL, fileManager: .default )
#expect(urls == [layout.gitdirURL(for: "sparrow"), layout.gitdirURL(for: "swift")]) }
@Test func fallsBackToEntryDirectoryWhenGitdirIsNotWrittenYet() throws { let layout = try RegistryLayout(entries: ["sparrow": .withGitdir, "swift": .directoryOnly]) defer { layout.remove() }
let urls = GitWorktreeRegistryMonitor.registryEntryURLs( inWorktreesDirectory: layout.worktreesDirectoryURL, fileManager: .default )
#expect(urls == [layout.gitdirURL(for: "sparrow"), layout.entryURL(for: "swift")]) }
@Test func ignoresFilesSittingDirectlyInTheWorktreesDirectory() throws { let layout = try RegistryLayout(entries: ["sparrow": .withGitdir]) defer { layout.remove() } try Data().write(to: layout.worktreesDirectoryURL.appending(path: "stray-file"))
let urls = GitWorktreeRegistryMonitor.registryEntryURLs( inWorktreesDirectory: layout.worktreesDirectoryURL, fileManager: .default )
#expect(urls == [layout.gitdirURL(for: "sparrow")]) }
@Test func returnsNoTargetsWhenTheRegistryDirectoryIsAbsent() throws { let layout = try RegistryLayout(entries: [:], createWorktreesDirectory: false) defer { layout.remove() }
let urls = GitWorktreeRegistryMonitor.registryEntryURLs( inWorktreesDirectory: layout.worktreesDirectoryURL, fileManager: .default )
#expect(urls.isEmpty) }}
@MainActorprivate final class RegistryEvents { private var pendingEvent: XCTestExpectation? private(set) var count = 0
func record() { count += 1 pendingEvent?.fulfill() pendingEvent = nil }
func expectEvent(_ operation: () throws -> Void) async throws { await drainEvents() let event = XCTestExpectation(description: "Registry change") pendingEvent = event defer { pendingEvent = nil } try operation() let result = await XCTWaiter.fulfillment(of: [event], timeout: 2) #expect(result == .completed) }
func drainEvents() async { // Vnode delivery uses the OS queue, not a test clock. Let prior events settle // before arming an expectation so they cannot satisfy the next operation. await withCheckedContinuation { continuation in DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { continuation.resume() } } }}
private struct RegistryLayout { enum Entry { case withGitdir case directoryOnly }
let rootURL: URL let worktreesDirectoryURL: URL
init(entries: [String: Entry], createWorktreesDirectory: Bool = true) throws { rootURL = FileManager.default.temporaryDirectory .appending(path: "registry-monitor-\(UUID().uuidString)") worktreesDirectoryURL = rootURL.appending(path: ".git").appending(path: "worktrees") if createWorktreesDirectory { try FileManager.default.createDirectory(at: worktreesDirectoryURL, withIntermediateDirectories: true) } else { try FileManager.default.createDirectory(at: rootURL, withIntermediateDirectories: true) } for (name, entry) in entries { try FileManager.default.createDirectory(at: entryURL(for: name), withIntermediateDirectories: true) if entry == .withGitdir { try Data("\(rootURL.path(percentEncoded: false))/\(name)/.git\n".utf8) .write(to: gitdirURL(for: name)) } } }
func entryURL(for name: String) -> URL { worktreesDirectoryURL.appending(path: name).standardizedFileURL }
func gitdirURL(for name: String) -> URL { entryURL(for: name).appending(path: "gitdir").standardizedFileURL }
func initializeGitWorktrees() throws { try git(["init"]) try git(["-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "--allow-empty", "-m", "Initial"]) try git(["worktree", "add", "-b", "sparrow", "sparrow"]) try git(["worktree", "add", "-b", "swift", "swift"]) }
func git(_ arguments: [String]) throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/git") process.arguments = [ "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", "-C", rootURL.path(percentEncoded: false), ] + arguments process.standardOutput = FileHandle.nullDevice process.standardError = FileHandle.nullDevice try process.run() process.waitUntilExit() try #require(process.terminationStatus == 0, "Git command failed: \(arguments)") }
func remove() { try? FileManager.default.removeItem(at: rootURL) }}