native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
13 kB · 357 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358import Foundationimport Testing
@testable import supacode
nonisolated final class GitShellInvocationRecorder: @unchecked Sendable { struct Snapshot { let executableURL: URL? let arguments: [String] let currentDirectoryURL: URL? }
private let lock = NSLock() private var executableURLValue: URL? private var argumentsValue: [String] = [] private var currentDirectoryURLValue: URL?
func record(executableURL: URL, arguments: [String], currentDirectoryURL: URL?) { lock.lock() executableURLValue = executableURL argumentsValue = arguments currentDirectoryURLValue = currentDirectoryURL lock.unlock() }
func snapshot() -> Snapshot { lock.lock() let value = Snapshot( executableURL: executableURLValue, arguments: argumentsValue, currentDirectoryURL: currentDirectoryURLValue ) lock.unlock() return value }}
struct GitClientCreateWorktreeStreamTests { private func makeRequest( name: String = "new-wt", repoRoot: URL, baseDirectory: URL? = nil, copyFiles: GitWorktreeCreateRequest.CopyFiles = GitWorktreeCreateRequest.CopyFiles( ignored: false, untracked: false ), baseRef: String = "", directoryOverride: URL? = nil ) -> GitWorktreeCreateRequest { GitWorktreeCreateRequest( name: name, repoRoot: repoRoot, baseDirectory: baseDirectory ?? URL(fileURLWithPath: "/tmp/repo/.worktrees"), copyFiles: copyFiles, baseRef: baseRef, directoryOverride: directoryOverride ) }
@Test func createWorktreeStreamAddsVerboseWhenCopyingFiles() async throws { let recorder = GitShellInvocationRecorder() let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { executableURL, arguments, currentDirectoryURL, _ in recorder.record( executableURL: executableURL, arguments: arguments, currentDirectoryURL: currentDirectoryURL ) return AsyncThrowingStream { continuation in continuation.yield(.line(ShellStreamLine(source: .stdout, text: "/tmp/repo/swift-otter"))) continuation.yield(.finished(ShellOutput(stdout: "/tmp/repo/swift-otter", stderr: "", exitCode: 0))) continuation.finish() } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
for try await _ in client.createWorktreeStream( makeRequest( name: "swift-otter", repoRoot: repoRoot, copyFiles: GitWorktreeCreateRequest.CopyFiles(ignored: true, untracked: false), baseRef: "origin/main" ) ) {}
let snapshot = recorder.snapshot() #expect(snapshot.currentDirectoryURL == repoRoot) #expect(snapshot.arguments.contains("sw")) if let baseDirFlagIndex = snapshot.arguments.firstIndex(of: "--base-dir") { #expect(snapshot.arguments.count > baseDirFlagIndex + 1) #expect(snapshot.arguments[baseDirFlagIndex + 1] == "/tmp/repo/.worktrees") } else { Issue.record("Expected --base-dir in createWorktreeStream arguments") } #expect(snapshot.arguments.contains("--copy-ignored")) #expect(snapshot.arguments.contains("--verbose")) #expect(snapshot.arguments.contains("--from")) #expect(snapshot.arguments.contains("origin/main")) #expect(snapshot.arguments.contains("swift-otter")) }
@Test func createWorktreeStreamForwardsOutputLines() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { _, _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.line(ShellStreamLine(source: .stderr, text: "[1/2] copy .env"))) continuation.yield(.line(ShellStreamLine(source: .stdout, text: "preparing"))) continuation.yield(.line(ShellStreamLine(source: .stdout, text: "/tmp/repo/swift-otter"))) continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo") var outputLines: [ShellStreamLine] = [] var finishedWorktree: Worktree? for try await event in client.createWorktreeStream( makeRequest( name: "swift-otter", repoRoot: repoRoot, copyFiles: GitWorktreeCreateRequest.CopyFiles(ignored: true, untracked: true) ) ) { switch event { case .outputLine(let line): outputLines.append(line) case .finished(let worktree): finishedWorktree = worktree } }
#expect(outputLines.count == 3) #expect(outputLines[0] == ShellStreamLine(source: .stderr, text: "[1/2] copy .env")) #expect(outputLines[1] == ShellStreamLine(source: .stdout, text: "preparing")) #expect(outputLines[2] == ShellStreamLine(source: .stdout, text: "/tmp/repo/swift-otter")) #expect(finishedWorktree?.id == "/tmp/repo/swift-otter") }
@Test func createWorktreeStreamUsesLastNonEmptyStdoutLineAsPath() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { _, _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.line(ShellStreamLine(source: .stdout, text: "creating"))) continuation.yield(.line(ShellStreamLine(source: .stdout, text: ""))) continuation.yield(.line(ShellStreamLine(source: .stdout, text: "/tmp/repo/new-wt"))) continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo") var finishedWorktree: Worktree? for try await event in client.createWorktreeStream( makeRequest(repoRoot: repoRoot) ) { if case .finished(let worktree) = event { finishedWorktree = worktree } }
#expect(finishedWorktree?.id == "/tmp/repo/new-wt") #expect(finishedWorktree?.workingDirectory == URL(fileURLWithPath: "/tmp/repo/new-wt")) }
@Test func createWorktreeStreamUsesFinishedOutputWhenNoLineEventsAreEmitted() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput( stdout: "creating worktree\n/tmp/repo/new-wt\n", stderr: "", exitCode: 0 ) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo") var outputLines: [ShellStreamLine] = [] var finishedWorktree: Worktree? for try await event in client.createWorktreeStream( makeRequest(repoRoot: repoRoot) ) { switch event { case .outputLine(let line): outputLines.append(line) case .finished(let worktree): finishedWorktree = worktree } }
#expect(outputLines.isEmpty) #expect(finishedWorktree?.id == "/tmp/repo/new-wt") #expect(finishedWorktree?.workingDirectory == URL(fileURLWithPath: "/tmp/repo/new-wt")) }
@Test func createWorktreeStreamThrowsWhenNoPathLineIsEmitted() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { _, _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.line(ShellStreamLine(source: .stderr, text: "[1/10] copy .env"))) continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
do { for try await _ in client.createWorktreeStream( makeRequest(repoRoot: repoRoot) ) {} Issue.record("Expected createWorktreeStream to throw when stdout path is missing") } catch let error as GitClientError { #expect(error.localizedDescription.contains("Empty output")) } }
@Test func createWorktreeWrapsShellClientError() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { _, _, _, _ in AsyncThrowingStream { continuation in continuation.finish( throwing: ShellClientError( command: "wt sw", stdout: "out", stderr: "err", exitCode: 1 ) ) } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
do { _ = try await client.createWorktree( named: "new-wt", in: repoRoot, baseDirectory: URL(fileURLWithPath: "/tmp/repo/.worktrees"), copyFiles: (ignored: false, untracked: false), baseRef: "" ) Issue.record("Expected createWorktree to throw") } catch let error as GitClientError { #expect(error.localizedDescription.contains("Git command failed")) #expect(error.localizedDescription.contains("stdout:\nout")) #expect(error.localizedDescription.contains("stderr:\nerr")) } }
@Test func createWorktreeReturnsFinishedWorktreeFromStream() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runStream: { _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.finished(ShellOutput(stdout: "", stderr: "", exitCode: 0))) continuation.finish() } }, runLoginStreamImpl: { _, _, _, _ in AsyncThrowingStream { continuation in continuation.yield(.line(ShellStreamLine(source: .stdout, text: "/tmp/repo/new-wt"))) continuation.yield(.finished(ShellOutput(stdout: "/tmp/repo/new-wt", stderr: "", exitCode: 0))) continuation.finish() } } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
let worktree = try await client.createWorktree( named: "new-wt", in: repoRoot, baseDirectory: URL(fileURLWithPath: "/tmp/repo/.worktrees"), copyFiles: (ignored: false, untracked: false), baseRef: "" )
#expect(worktree.id == "/tmp/repo/new-wt") #expect(worktree.name == "new-wt") #expect(worktree.repositoryRootURL == repoRoot) }
@Test func createWorktreeUsesFinishedOutputWhenNoLineEventsAreEmitted() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput( stdout: "preparing\n/tmp/repo/new-wt\n", stderr: "", exitCode: 0 ) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
let worktree = try await client.createWorktree( named: "new-wt", in: repoRoot, baseDirectory: URL(fileURLWithPath: "/tmp/repo/.worktrees"), copyFiles: (ignored: false, untracked: false), baseRef: "" )
#expect(worktree.id == "/tmp/repo/new-wt") #expect(worktree.name == "new-wt") #expect(worktree.repositoryRootURL == repoRoot) }}