native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264import Foundationimport Testing
@testable import Prowl
actor ShellCallStore { private(set) var calls: [[String]] = []
func record(_ arguments: [String]) { calls.append(arguments) }}
struct GitClientBranchRefsTests { @Test func remoteMatcherUsesRemotePrefix() { let remote = GitRemoteMatcher.matchingRemote( for: "origin/main", from: ["origin", "upstream"] )
#expect(remote == "origin") }
@Test func remoteMatcherUsesLongestRemotePrefix() { let remote = GitRemoteMatcher.matchingRemote( for: "origin-fork/main", from: ["origin", "origin-fork"] )
#expect(remote == "origin-fork") }
@Test func remoteMatcherReturnsNilForLocalBranch() { let remote = GitRemoteMatcher.matchingRemote( for: "local-branch", from: ["origin"] )
#expect(remote == nil) }
@Test func branchRefsIncludesLocalAndRemoteTrackingRefs() async throws { let store = ShellCallStore() let shell = ShellClient( run: { _, arguments, _ in await store.record(arguments) if arguments.contains("refs/heads") { return ShellOutput( stdout: "refs/heads/feature\nrefs/heads/main\nrefs/heads/bugfix\n", stderr: "", exitCode: 0 ) } return ShellOutput( stdout: "refs/remotes/origin/feature\nrefs/remotes/origin/bugfix\n", stderr: "", exitCode: 0 ) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
let refs = try await client.branchRefs(for: repoRoot)
let expected = ["bugfix", "feature", "main", "origin/bugfix", "origin/feature"] .sorted { $0.localizedStandardCompare($1) == .orderedAscending } #expect(refs == expected) let options = try await client.branchRefOptions(for: repoRoot) #expect(options.contains(GitBranchRefOption(ref: "main", kind: .local))) #expect(options.contains(GitBranchRefOption(ref: "origin/feature", kind: .remoteTracking))) let calls = await store.calls #expect(calls.count == 4) let args = calls[0] #expect(args.contains("/usr/bin/git")) #expect(args.contains("PATH=/usr/bin:/bin")) #expect(args.contains("for-each-ref")) #expect(args.contains("refs/heads")) #expect(args.contains("--format=%(refname)")) #expect(calls[1].contains("refs/remotes")) }
@Test func branchRefOptionsOmitsRemoteHeadPointer() async throws { let shell = ShellClient( run: { _, arguments, _ in if arguments.contains("refs/heads") { return ShellOutput(stdout: "refs/heads/main\n", stderr: "", exitCode: 0) } return ShellOutput( stdout: "refs/remotes/origin/HEAD\nrefs/remotes/origin/main\n", stderr: "", exitCode: 0 ) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
let refs = try await client.branchRefs(for: repoRoot) let options = try await client.branchRefOptions(for: repoRoot)
#expect(refs == ["main", "origin/main"]) // `<remote>/HEAD` pointers must be filtered so the remote-tracking checkout // path never derives an invalid `HEAD` branch name. #expect(!options.contains(GitBranchRefOption(ref: "origin/HEAD", kind: .remoteTracking))) #expect(options.contains(GitBranchRefOption(ref: "origin/main", kind: .remoteTracking))) }
@Test func branchRefOptionsPreservesLocalBranchNamedHEAD() async throws { let shell = ShellClient( run: { _, arguments, _ in if arguments.contains("refs/heads") { return ShellOutput( stdout: "refs/heads/main\nrefs/heads/fix/HEAD\n", stderr: "", exitCode: 0 ) } return ShellOutput( stdout: "refs/remotes/origin/HEAD\nrefs/remotes/origin/main\n", stderr: "", exitCode: 0 ) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable }) let repoRoot = URL(fileURLWithPath: "/tmp/repo")
let options = try await client.branchRefOptions(for: repoRoot)
#expect( options.contains(GitBranchRefOption(ref: "fix/HEAD", kind: .local)), "Local branch fix/HEAD must not be filtered out" ) #expect( !options.contains(GitBranchRefOption(ref: "origin/HEAD", kind: .remoteTracking)), "Remote HEAD pointer should still be filtered" ) }
@Test func remoteBranchRefsParsesDefaultHeadAndBranches() async throws { let output = """ ref: refs/heads/main\tHEAD abc123\tHEAD def456\trefs/heads/develop abc123\trefs/heads/main """ let shell = ShellClient( run: { _, arguments, _ in #expect( arguments == GitExecutable.testExecutable.environmentArguments + [ "/usr/bin/git", "ls-remote", "--symref", "--end-of-options", "git@github.com:onevcat/app.git", "HEAD", "refs/heads/*", ]) return ShellOutput(stdout: output, stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let refs = try await client.remoteBranchRefs(for: "git@github.com:onevcat/app.git")
#expect(refs.defaultBaseRef == "origin/main") #expect( refs.options == [ GitBranchRefOption(ref: "origin/develop", kind: .fetchedRemote), GitBranchRefOption(ref: "origin/main", kind: .fetchedRemote), ]) }
@Test func defaultRemoteBranchRefStripsPrefix() async throws { let shell = ShellClient( run: { _, _, _ in ShellOutput(stdout: "refs/remotes/origin/develop\n", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let ref = try await client.defaultRemoteBranchRef(for: URL(fileURLWithPath: "/tmp/repo"))
#expect(ref == "origin/develop") }
@Test func defaultRemoteBranchRefReturnsNilOnError() async throws { let shell = ShellClient( run: { _, _, _ in throw ShellClientError(command: "git", stdout: "", stderr: "boom", exitCode: 1) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let ref = try await client.defaultRemoteBranchRef(for: URL(fileURLWithPath: "/tmp/repo"))
#expect(ref == nil) }
@Test func defaultRemoteBranchRefFallsBackToOriginMain() async throws { let shell = ShellClient( run: { _, arguments, _ in if arguments.contains("symbolic-ref") { throw ShellClientError(command: "git", stdout: "", stderr: "missing", exitCode: 1) } if arguments.contains("rev-parse") { return ShellOutput(stdout: "hash", stderr: "", exitCode: 0) } return ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let ref = try await client.defaultRemoteBranchRef(for: URL(fileURLWithPath: "/tmp/repo"))
#expect(ref == "origin/main") }
@Test func automaticWorktreeBaseRefUsesResolvedDefault() async throws { let shell = ShellClient( run: { _, arguments, _ in if arguments.contains("symbolic-ref") { return ShellOutput(stdout: "refs/remotes/origin/develop\n", stderr: "", exitCode: 0) } if arguments.contains("rev-parse") { return ShellOutput(stdout: "hash", stderr: "", exitCode: 0) } return ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let ref = await client.automaticWorktreeBaseRef(for: URL(fileURLWithPath: "/tmp/repo"))
#expect(ref == "origin/develop") }
@Test func automaticWorktreeBaseRefReturnsNilWhenUnavailable() async throws { let shell = ShellClient( run: { _, arguments, _ in if arguments.contains("symbolic-ref") || arguments.contains("rev-parse") { throw ShellClientError(command: "git", stdout: "", stderr: "missing", exitCode: 1) } return ShellOutput(stdout: "", stderr: "", exitCode: 0) }, runLoginImpl: { _, _, _, _ in ShellOutput(stdout: "", stderr: "", exitCode: 0) } ) let client = GitClient(shell: shell, resolveGit: { _ in .testExecutable })
let ref = await client.automaticWorktreeBaseRef(for: URL(fileURLWithPath: "/tmp/repo"))
#expect(ref == nil) }}