From 0c8b5ed59a77e234da29f94f859e096b5db18686 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 13 Dec 2025 18:08:47 +0000 Subject: [PATCH] test(mac): cover codesign + node manager paths --- .../Sources/Clawdis/ControlSocketServer.swift | 29 ++++++- apps/macos/Sources/Clawdis/Utilities.swift | 83 ++++++++++++++++++- .../ControlSocketServerTests.swift | 49 +++++++++++ .../NodeManagerPathsTests.swift | 46 ++++++++++ 4 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 apps/macos/Tests/ClawdisIPCTests/ControlSocketServerTests.swift create mode 100644 apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift diff --git a/apps/macos/Sources/Clawdis/ControlSocketServer.swift b/apps/macos/Sources/Clawdis/ControlSocketServer.swift index b55f5193a..e72d539af 100644 --- a/apps/macos/Sources/Clawdis/ControlSocketServer.swift +++ b/apps/macos/Sources/Clawdis/ControlSocketServer.swift @@ -272,7 +272,9 @@ final actor ControlSocketServer { let sCode = staticCode else { return false } var infoCF: CFDictionary? - guard SecCodeCopySigningInformation(sCode, SecCSFlags(), &infoCF) == errSecSuccess, + // `kSecCodeInfoTeamIdentifier` is only included when requesting signing information. + let flags = SecCSFlags(rawValue: UInt32(kSecCSSigningInformation)) + guard SecCodeCopySigningInformation(sCode, flags, &infoCF) == errSecSuccess, let info = infoCF as? [String: Any], let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String else { @@ -282,3 +284,28 @@ final actor ControlSocketServer { return allowedTeamIDs.contains(teamID) } } + +#if SWIFT_PACKAGE +extension ControlSocketServer { + nonisolated static func _testTeamIdentifier(pid: pid_t) -> String? { + let attrs: NSDictionary = [kSecGuestAttributePid: pid] + var secCode: SecCode? + guard SecCodeCopyGuestWithAttributes(nil, attrs, SecCSFlags(), &secCode) == errSecSuccess, + let code = secCode else { return nil } + + var staticCode: SecStaticCode? + guard SecCodeCopyStaticCode(code, SecCSFlags(), &staticCode) == errSecSuccess, + let sCode = staticCode else { return nil } + + var infoCF: CFDictionary? + let flags = SecCSFlags(rawValue: UInt32(kSecCSSigningInformation)) + guard SecCodeCopySigningInformation(sCode, flags, &infoCF) == errSecSuccess, + let info = infoCF as? [String: Any] + else { + return nil + } + + return info[kSecCodeInfoTeamIdentifier as String] as? String + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/Utilities.swift b/apps/macos/Sources/Clawdis/Utilities.swift index 86e4ab809..315afa38d 100644 --- a/apps/macos/Sources/Clawdis/Utilities.swift +++ b/apps/macos/Sources/Clawdis/Utilities.swift @@ -264,19 +264,92 @@ enum CommandResolver { static func preferredPaths() -> [String] { let current = ProcessInfo.processInfo.environment["PATH"]? .split(separator: ":").map(String.init) ?? [] + let home = FileManager.default.homeDirectoryForCurrentUser + let projectRoot = self.projectRoot() + return self.preferredPaths(home: home, current: current, projectRoot: projectRoot) + } + + static func preferredPaths(home: URL, current: [String], projectRoot: URL) -> [String] { var extras = [ - FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/pnpm").path, + home.appendingPathComponent("Library/pnpm").path, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", ] - extras.insert(self.projectRoot().appendingPathComponent("node_modules/.bin").path, at: 0) + extras.insert(projectRoot.appendingPathComponent("node_modules/.bin").path, at: 0) + extras.insert(contentsOf: self.nodeManagerBinPaths(home: home), at: 1) var seen = Set() // Preserve order while stripping duplicates so PATH lookups remain deterministic. return (extras + current).filter { seen.insert($0).inserted } } + private static func nodeManagerBinPaths(home: URL) -> [String] { + var bins: [String] = [] + + // Volta + let volta = home.appendingPathComponent(".volta/bin") + if FileManager.default.fileExists(atPath: volta.path) { + bins.append(volta.path) + } + + // asdf + let asdf = home.appendingPathComponent(".asdf/shims") + if FileManager.default.fileExists(atPath: asdf.path) { + bins.append(asdf.path) + } + + // fnm + bins.append(contentsOf: self.versionedNodeBinPaths( + base: home.appendingPathComponent(".local/share/fnm/node-versions"), + suffix: "installation/bin")) + + // nvm + bins.append(contentsOf: self.versionedNodeBinPaths( + base: home.appendingPathComponent(".nvm/versions/node"), + suffix: "bin")) + + return bins + } + + private static func versionedNodeBinPaths(base: URL, suffix: String) -> [String] { + guard FileManager.default.fileExists(atPath: base.path) else { return [] } + let entries: [String] + do { + entries = try FileManager.default.contentsOfDirectory(atPath: base.path) + } catch { + return [] + } + + func parseVersion(_ name: String) -> [Int] { + let trimmed = name.hasPrefix("v") ? String(name.dropFirst()) : name + return trimmed.split(separator: ".").compactMap { Int($0) } + } + + let sorted = entries.sorted { a, b in + let va = parseVersion(a) + let vb = parseVersion(b) + let maxCount = max(va.count, vb.count) + for i in 0.. bi } + } + // If identical numerically, keep stable ordering. + return a > b + } + + var paths: [String] = [] + for entry in sorted { + let binDir = base.appendingPathComponent(entry).appendingPathComponent(suffix) + let node = binDir.appendingPathComponent("node") + if FileManager.default.isExecutableFile(atPath: node.path) { + paths.append(binDir.path) + } + } + return paths + } + static func findExecutable(named name: String) -> String? { for dir in self.preferredPaths() { let candidate = (dir as NSString).appendingPathComponent(name) @@ -565,4 +638,10 @@ enum CommandResolver { } return URL(fileURLWithPath: expanded) } + +#if SWIFT_PACKAGE + static func _testNodeManagerBinPaths(home: URL) -> [String] { + self.nodeManagerBinPaths(home: home) + } +#endif } diff --git a/apps/macos/Tests/ClawdisIPCTests/ControlSocketServerTests.swift b/apps/macos/Tests/ClawdisIPCTests/ControlSocketServerTests.swift new file mode 100644 index 000000000..3ba020044 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/ControlSocketServerTests.swift @@ -0,0 +1,49 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct ControlSocketServerTests { + private static func codesignTeamIdentifier(executablePath: String) -> String? { + let proc = Process() + proc.executableURL = URL(fileURLWithPath: "/usr/bin/codesign") + proc.arguments = ["-dv", "--verbose=4", executablePath] + proc.standardOutput = Pipe() + let stderr = Pipe() + proc.standardError = stderr + + do { + try proc.run() + proc.waitUntilExit() + } catch { + return nil + } + + guard proc.terminationStatus == 0 else { + return nil + } + + let data = stderr.fileHandleForReading.readDataToEndOfFile() + guard let text = String(data: data, encoding: .utf8) else { return nil } + for line in text.split(separator: "\n") { + if line.hasPrefix("TeamIdentifier=") { + let raw = String(line.dropFirst("TeamIdentifier=".count)).trimmingCharacters(in: .whitespacesAndNewlines) + return raw == "not set" ? nil : raw + } + } + return nil + } + + @Test func teamIdentifierLookupMatchesCodesign() async { + let pid = getpid() + let execPath = CommandLine.arguments.first ?? "" + + let expected = Self.codesignTeamIdentifier(executablePath: execPath) + let actual = ControlSocketServer._testTeamIdentifier(pid: pid) + + if let expected, !expected.isEmpty { + #expect(actual == expected) + } else { + #expect(actual == nil || actual?.isEmpty == true) + } + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift b/apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift new file mode 100644 index 000000000..19c31708c --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift @@ -0,0 +1,46 @@ +import Foundation +import Testing +@testable import Clawdis + +@Suite struct NodeManagerPathsTests { + private func makeTempDir() throws -> URL { + let base = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true) + let dir = base.appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + return dir + } + + private func makeExec(at path: URL) throws { + try FileManager.default.createDirectory( + at: path.deletingLastPathComponent(), + withIntermediateDirectories: true) + FileManager.default.createFile(atPath: path.path, contents: Data("echo ok\n".utf8)) + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: path.path) + } + + @Test func fnmNodeBinsPreferNewestInstalledVersion() throws { + let home = try self.makeTempDir() + + let v20Bin = home + .appendingPathComponent(".local/share/fnm/node-versions/v20.19.5/installation/bin/node") + let v25Bin = home + .appendingPathComponent(".local/share/fnm/node-versions/v25.1.0/installation/bin/node") + try self.makeExec(at: v20Bin) + try self.makeExec(at: v25Bin) + + let bins = CommandResolver._testNodeManagerBinPaths(home: home) + #expect(bins.first == v25Bin.deletingLastPathComponent().path) + #expect(bins.contains(v20Bin.deletingLastPathComponent().path)) + } + + @Test func ignoresEntriesWithoutNodeExecutable() throws { + let home = try self.makeTempDir() + let missingNodeBin = home + .appendingPathComponent(".local/share/fnm/node-versions/v99.0.0/installation/bin") + try FileManager.default.createDirectory(at: missingNodeBin, withIntermediateDirectories: true) + + let bins = CommandResolver._testNodeManagerBinPaths(home: home) + #expect(!bins.contains(missingNodeBin.path)) + } +} +