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 }
|
||||
|
||||
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
|
||||
|
||||
@@ -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<String>()
|
||||
// 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..<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? {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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