test(mac): cover codesign + node manager paths
This commit is contained in:
@@ -272,7 +272,9 @@ final actor ControlSocketServer {
|
|||||||
let sCode = staticCode else { return false }
|
let sCode = staticCode else { return false }
|
||||||
|
|
||||||
var infoCF: CFDictionary?
|
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 info = infoCF as? [String: Any],
|
||||||
let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String
|
let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String
|
||||||
else {
|
else {
|
||||||
@@ -282,3 +284,28 @@ final actor ControlSocketServer {
|
|||||||
return allowedTeamIDs.contains(teamID)
|
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
|
||||||
|
|||||||
@@ -264,19 +264,92 @@ enum CommandResolver {
|
|||||||
static func preferredPaths() -> [String] {
|
static func preferredPaths() -> [String] {
|
||||||
let current = ProcessInfo.processInfo.environment["PATH"]?
|
let current = ProcessInfo.processInfo.environment["PATH"]?
|
||||||
.split(separator: ":").map(String.init) ?? []
|
.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 = [
|
var extras = [
|
||||||
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/pnpm").path,
|
home.appendingPathComponent("Library/pnpm").path,
|
||||||
"/opt/homebrew/bin",
|
"/opt/homebrew/bin",
|
||||||
"/usr/local/bin",
|
"/usr/local/bin",
|
||||||
"/usr/bin",
|
"/usr/bin",
|
||||||
"/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<String>()
|
var seen = Set<String>()
|
||||||
// Preserve order while stripping duplicates so PATH lookups remain deterministic.
|
// Preserve order while stripping duplicates so PATH lookups remain deterministic.
|
||||||
return (extras + current).filter { seen.insert($0).inserted }
|
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..<maxCount {
|
||||||
|
let ai = i < va.count ? va[i] : 0
|
||||||
|
let bi = i < vb.count ? vb[i] : 0
|
||||||
|
if ai != bi { return ai > 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? {
|
static func findExecutable(named name: String) -> String? {
|
||||||
for dir in self.preferredPaths() {
|
for dir in self.preferredPaths() {
|
||||||
let candidate = (dir as NSString).appendingPathComponent(name)
|
let candidate = (dir as NSString).appendingPathComponent(name)
|
||||||
@@ -565,4 +638,10 @@ enum CommandResolver {
|
|||||||
}
|
}
|
||||||
return URL(fileURLWithPath: expanded)
|
return URL(fileURLWithPath: expanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SWIFT_PACKAGE
|
||||||
|
static func _testNodeManagerBinPaths(home: URL) -> [String] {
|
||||||
|
self.nodeManagerBinPaths(home: home)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift
Normal file
46
apps/macos/Tests/ClawdisIPCTests/NodeManagerPathsTests.swift
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user