native macOS codings agent orchestrator prowl.onev.cat
Something went wrong. Try again.
15 kB · 407 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408import Foundationimport ProwlCLISharedimport Testing
@testable import supacode
struct CLIInstallClientTests { private func makeTempDir() throws -> URL { let tmp = FileManager.default.temporaryDirectory .appendingPathComponent("prowl-cli-test-\(UUID().uuidString)") try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true) return tmp }
private func cleanup(_ url: URL) { try? FileManager.default.removeItem(at: url) }
/// The workflow gate needs a command a shell can run, not just an occupied slot. @Test func onlyAnExecutableCommandIsUsable() throws { let tmp = try makeTempDir() defer { cleanup(tmp) } let slot = tmp.appendingPathComponent("prowl") let client = CLIInstallClient.liveValue
#expect(!client.isUsable(slot))
try Data("#!/bin/sh\n".utf8).write(to: slot) #expect(!client.isUsable(slot)) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: slot.path) #expect(client.isUsable(slot))
try FileManager.default.removeItem(at: slot) try FileManager.default.createDirectory(at: slot, withIntermediateDirectories: false) #expect(!client.isUsable(slot))
try FileManager.default.removeItem(at: slot) let target = tmp.appendingPathComponent("real-prowl") try Data("#!/bin/sh\n".utf8).write(to: target) try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: target.path) try FileManager.default.createSymbolicLink(at: slot, withDestinationURL: target) #expect(client.isUsable(slot))
try FileManager.default.removeItem(at: target) #expect(!client.isUsable(slot)) }
@Test func installActionTitleFollowsTheSlot() { #expect(CLIInstallStatus.notInstalled.installActionTitle == "Install") #expect(CLIInstallStatus.broken(path: "/p", destination: "/gone").installActionTitle == "Repair") #expect( CLIInstallStatus.installedDifferentSource(path: "/p", destination: "/other").installActionTitle == "Reinstall") #expect(CLIInstallStatus.installed(path: "/p").installActionTitle == "Reinstall") // A real file or directory is never replaced by the installer, so no button is offered; the // copy says to remove it by hand. let occupied = CLIInstallStatus.installedDifferentSource(path: "/p", destination: nil) #expect(occupied.installActionTitle == nil) #expect(occupied.workflowBlockerCopy.contains("Remove it")) #expect(CLIInstallStatus.notInstalled.workflowBlockerCopy.contains("installed")) }
@Test func statusNotInstalledWhenNoFileExists() throws { let tmp = try makeTempDir() defer { cleanup(tmp) } let installPath = tmp.appendingPathComponent("prowl")
let client = CLIInstallClient.liveValue let status = client.installationStatus(installPath)
#expect(status == .notInstalled) }
@Test func statusInstalledWhenSymlinkPointsToBundle() throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let fakeBundledBinary = tmp.appendingPathComponent("bundled-prowl") FileManager.default.createFile(atPath: fakeBundledBinary.path, contents: nil)
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: fakeBundledBinary.path )
let client = CLIInstallClient( bundledCLIURL: { fakeBundledBinary }, installationStatus: { path in SymlinkInstaller.status( linkPath: path.path(percentEncoded: false), source: fakeBundledBinary.path(percentEncoded: false) ) }, isUsable: { _ in true }, install: { _ in }, uninstall: { _ in } )
let status = client.installationStatus(installPath) #expect(status == .installed(path: installPath.path)) }
@Test func statusDifferentSourceWhenSymlinkPointsElsewhere() throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let otherBinary = tmp.appendingPathComponent("other-prowl") FileManager.default.createFile(atPath: otherBinary.path, contents: nil)
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: otherBinary.path )
let client = CLIInstallClient.liveValue let status = client.installationStatus(installPath)
#expect(status == .installedDifferentSource(path: installPath.path, destination: otherBinary.path)) }
@Test func statusDifferentSourceWhenRegularFileExists() throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") FileManager.default.createFile(atPath: installPath.path, contents: Data("binary".utf8))
let client = CLIInstallClient.liveValue let status = client.installationStatus(installPath)
#expect(status == .installedDifferentSource(path: installPath.path, destination: nil)) }
@Test func statusBrokenWhenSymlinkIsDangling() throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: tmp.appendingPathComponent("moved-away").path )
let client = CLIInstallClient.liveValue let status = client.installationStatus(installPath)
#expect(status == .broken(path: installPath.path, destination: tmp.appendingPathComponent("moved-away").path)) }
@Test func liveInstallReplacesDanglingSymlinkWithBundledCLI() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: tmp.appendingPathComponent("moved-away").path )
let client = CLIInstallClient.liveValue let bundled = try #require(client.bundledCLIURL()) try await client.install(installPath)
let target = try FileManager.default.destinationOfSymbolicLink(atPath: installPath.path) #expect(target == bundled.path(percentEncoded: false)) #expect(client.installationStatus(installPath) == .installed(path: installPath.path)) }
@Test func liveUninstallRemovesDanglingSymlink() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: tmp.appendingPathComponent("moved-away").path )
try await CLIInstallClient.liveValue.uninstall(installPath)
#expect((try? FileManager.default.attributesOfItem(atPath: installPath.path)) == nil) }
@Test func installCreatesSymlink() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let fakeBundled = tmp.appendingPathComponent("source/prowl") try FileManager.default.createDirectory( at: fakeBundled.deletingLastPathComponent(), withIntermediateDirectories: true ) FileManager.default.createFile(atPath: fakeBundled.path, contents: Data("cli".utf8))
let installPath = tmp.appendingPathComponent("bin/prowl") let client = CLIInstallClient( bundledCLIURL: { fakeBundled }, installationStatus: CLIInstallClient.liveValue.installationStatus, isUsable: { _ in true }, install: { path in let fileManager = FileManager.default let bundledPath = fakeBundled.path(percentEncoded: false) let installDir = path.deletingLastPathComponent().path(percentEncoded: false) if !fileManager.fileExists(atPath: installDir) { try fileManager.createDirectory(atPath: installDir, withIntermediateDirectories: true) } let destination = path.path(percentEncoded: false) if fileManager.fileExists(atPath: destination) { try fileManager.removeItem(atPath: destination) } do { try fileManager.createSymbolicLink(atPath: destination, withDestinationPath: bundledPath) } catch { throw CLIInstallError(message: error.localizedDescription) } }, uninstall: CLIInstallClient.liveValue.uninstall )
try await client.install(installPath)
let fileManager = FileManager.default let attrs = try fileManager.attributesOfItem(atPath: installPath.path) #expect(attrs[.type] as? FileAttributeType == .typeSymbolicLink)
let target = try fileManager.destinationOfSymbolicLink(atPath: installPath.path) #expect(target == fakeBundled.path) }
@Test func installCreatesParentDirectoryIfNeeded() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let fakeBundled = tmp.appendingPathComponent("source/prowl") try FileManager.default.createDirectory( at: fakeBundled.deletingLastPathComponent(), withIntermediateDirectories: true ) FileManager.default.createFile(atPath: fakeBundled.path, contents: nil)
let deepPath = tmp.appendingPathComponent("a/b/c/prowl") let client = CLIInstallClient( bundledCLIURL: { fakeBundled }, installationStatus: CLIInstallClient.liveValue.installationStatus, isUsable: { _ in true }, install: { path in let fileManager = FileManager.default let bundledPath = fakeBundled.path(percentEncoded: false) let installDir = path.deletingLastPathComponent().path(percentEncoded: false) if !fileManager.fileExists(atPath: installDir) { try fileManager.createDirectory(atPath: installDir, withIntermediateDirectories: true) } let destination = path.path(percentEncoded: false) do { try fileManager.createSymbolicLink(atPath: destination, withDestinationPath: bundledPath) } catch { throw CLIInstallError(message: error.localizedDescription) } }, uninstall: CLIInstallClient.liveValue.uninstall )
try await client.install(deepPath)
#expect(FileManager.default.fileExists(atPath: deepPath.path)) }
@Test func uninstallRemovesSymlink() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let target = tmp.appendingPathComponent("target") FileManager.default.createFile(atPath: target.path, contents: nil)
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: target.path )
let client = CLIInstallClient.liveValue try await client.uninstall(installPath)
#expect(!FileManager.default.fileExists(atPath: installPath.path)) }
@Test func uninstallRefusesToRemoveRegularFile() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") FileManager.default.createFile(atPath: installPath.path, contents: Data("binary".utf8))
let client = CLIInstallClient.liveValue
do { try await client.uninstall(installPath) Issue.record("Expected uninstall to throw for non-symlink file") } catch let error as CLIInstallError { #expect(error.message.contains("not a symlink")) } #expect(FileManager.default.fileExists(atPath: installPath.path)) }
@Test func installRefusesToOverwriteRegularFile() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let fakeBundled = tmp.appendingPathComponent("source/prowl") try FileManager.default.createDirectory( at: fakeBundled.deletingLastPathComponent(), withIntermediateDirectories: true ) FileManager.default.createFile(atPath: fakeBundled.path, contents: Data("cli".utf8))
let installPath = tmp.appendingPathComponent("prowl") FileManager.default.createFile(atPath: installPath.path, contents: Data("existing".utf8))
let client = CLIInstallClient.liveValue
do { try await client.install(installPath) Issue.record("Expected install to throw for non-symlink target") } catch let error as CLIInstallError { #expect(error.message.contains("not a symlink")) } // Original file must be preserved let contents = FileManager.default.contents(atPath: installPath.path) #expect(contents == Data("existing".utf8)) }
@Test func installOverwritesExistingSymlink() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let fakeBundled = tmp.appendingPathComponent("source/prowl") try FileManager.default.createDirectory( at: fakeBundled.deletingLastPathComponent(), withIntermediateDirectories: true ) FileManager.default.createFile(atPath: fakeBundled.path, contents: Data("cli".utf8))
let oldTarget = tmp.appendingPathComponent("old-prowl") FileManager.default.createFile(atPath: oldTarget.path, contents: nil)
let installPath = tmp.appendingPathComponent("prowl") try FileManager.default.createSymbolicLink( atPath: installPath.path, withDestinationPath: oldTarget.path )
let client = makeTestInstallClient(bundledBinary: fakeBundled) try await client.install(installPath)
let newTarget = try FileManager.default.destinationOfSymbolicLink(atPath: installPath.path) #expect(newTarget == fakeBundled.path) }
@Test func uninstallThrowsWhenNoFileExists() async throws { let tmp = try makeTempDir() defer { cleanup(tmp) }
let installPath = tmp.appendingPathComponent("prowl") let client = CLIInstallClient.liveValue
do { try await client.uninstall(installPath) Issue.record("Expected uninstall to throw when file does not exist") } catch let error as CLIInstallError { #expect(error.message.contains("No CLI tool found")) } }
/// Creates a test install client that uses a fake bundled binary path instead of Bundle.main. private func makeTestInstallClient(bundledBinary: URL) -> CLIInstallClient { CLIInstallClient( bundledCLIURL: { bundledBinary }, installationStatus: CLIInstallClient.liveValue.installationStatus, isUsable: { _ in true }, install: { installPath in let fileManager = FileManager.default let bundledPath = bundledBinary.path(percentEncoded: false) guard fileManager.fileExists(atPath: bundledPath) else { throw CLIInstallError(message: "Bundled CLI binary not found.") } let installDir = installPath.deletingLastPathComponent().path(percentEncoded: false) if !fileManager.fileExists(atPath: installDir) { try fileManager.createDirectory(atPath: installDir, withIntermediateDirectories: true) } let destination = installPath.path(percentEncoded: false) if fileManager.fileExists(atPath: destination) { let attrs = try? fileManager.attributesOfItem(atPath: destination) let isSymlink = attrs?[.type] as? FileAttributeType == .typeSymbolicLink guard isSymlink else { throw CLIInstallError( message: "A file already exists at \(destination) and is not a symlink." ) } try fileManager.removeItem(atPath: destination) } try fileManager.createSymbolicLink(atPath: destination, withDestinationPath: bundledPath) }, uninstall: CLIInstallClient.liveValue.uninstall ) }}