test: add mac coverage helpers

This commit is contained in:
Peter Steinberger
2025-12-24 19:29:44 +01:00
parent f44014ff00
commit 204bd7d2c4
11 changed files with 632 additions and 71 deletions

View File

@@ -240,3 +240,20 @@ final class CanvasSchemeHandler: NSObject, WKURLSchemeHandler {
}
}
}
#if DEBUG
extension CanvasSchemeHandler {
func _testResponse(for url: URL) -> (mime: String, data: Data) {
let response = self.response(for: url)
return (response.mime, response.data)
}
func _testResolveFileURL(sessionRoot: URL, requestPath: String) -> URL? {
self.resolveFileURL(sessionRoot: sessionRoot, requestPath: requestPath)
}
func _testTextEncodingName(for mimeType: String) -> String? {
self.textEncodingName(forMimeType: mimeType)
}
}
#endif

View File

@@ -748,7 +748,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
}
}
private static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
fileprivate static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return false
}
@@ -766,7 +766,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
return false
}
private static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
fileprivate static func parseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
let parts = host.split(separator: ".", omittingEmptySubsequences: false)
guard parts.count == 4 else { return nil }
let bytes: [UInt8] = parts.compactMap { UInt8($0) }
@@ -774,7 +774,7 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan
return (bytes[0], bytes[1], bytes[2], bytes[3])
}
private static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
fileprivate static func isLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
let (a, b, _, _) = ip
if a == 10 { return true }
if a == 172, (16...31).contains(Int(b)) { return true }
@@ -1012,3 +1012,40 @@ private final class HoverChromeContainerView: NSView {
}
}
}
#if DEBUG
extension CanvasWindowController {
static func _testSanitizeSessionKey(_ key: String) -> String {
self.sanitizeSessionKey(key)
}
static func _testJSStringLiteral(_ value: String) -> String {
self.jsStringLiteral(value)
}
static func _testJSOptionalStringLiteral(_ value: String?) -> String {
self.jsOptionalStringLiteral(value)
}
static func _testStoredFrameKey(sessionKey: String) -> String {
self.storedFrameDefaultsKey(sessionKey: sessionKey)
}
static func _testStoreAndLoadFrame(sessionKey: String, frame: NSRect) -> NSRect? {
self.storeRestoredFrame(frame, sessionKey: sessionKey)
return self.loadRestoredFrame(sessionKey: sessionKey)
}
static func _testParseIPv4(_ host: String) -> (UInt8, UInt8, UInt8, UInt8)? {
CanvasA2UIActionMessageHandler.parseIPv4(host)
}
static func _testIsLocalNetworkIPv4(_ ip: (UInt8, UInt8, UInt8, UInt8)) -> Bool {
CanvasA2UIActionMessageHandler.isLocalNetworkIPv4(ip)
}
static func _testIsLocalNetworkCanvasURL(_ url: URL) -> Bool {
CanvasA2UIActionMessageHandler.isLocalNetworkCanvasURL(url)
}
}
#endif

View File

@@ -874,4 +874,56 @@ struct DebugSettings_Previews: PreviewProvider {
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
@MainActor
extension DebugSettings {
static func exerciseForTesting() async {
var view = DebugSettings()
view.modelsCount = 3
view.modelsLoading = false
view.modelsError = "Failed to load models"
view.gatewayRootInput = "/tmp/clawdis"
view.sessionStorePath = "/tmp/sessions.json"
view.sessionStoreSaveError = "Save failed"
view.debugSendInFlight = true
view.debugSendStatus = "Sent"
view.debugSendError = "Failed"
view.portCheckInFlight = true
view.portReports = [
DebugActions.PortReport(
port: 18789,
expected: "Gateway websocket (node/tsx)",
status: .missing("Missing"),
listeners: []),
]
view.portKillStatus = "Killed"
view.pendingKill = DebugActions.PortListener(
pid: 1,
command: "node",
fullCommand: "node",
user: nil,
expected: true)
view.canvasSessionKey = "main"
view.canvasStatus = "Canvas ok"
view.canvasError = "Canvas error"
view.canvasEvalJS = "document.title"
view.canvasEvalResult = "Canvas"
view.canvasSnapshotPath = "/tmp/snapshot.png"
_ = view.body
_ = view.header
_ = view.appInfoSection
_ = view.gatewaySection
_ = view.logsSection
_ = view.portsSection
_ = view.pathsSection
_ = view.quickActionsSection
_ = view.canvasSection
_ = view.experimentsSection
_ = view.gridLabel("Test")
view.loadSessionStorePath()
await view.reloadModels()
}
}
#endif

View File

@@ -159,3 +159,23 @@ enum GatewayLaunchAgentManager {
}.value
}
}
#if DEBUG
extension GatewayLaunchAgentManager {
static func _testGatewayExecutablePath(bundlePath: String) -> String {
self.gatewayExecutablePath(bundlePath: bundlePath)
}
static func _testRelayDir(bundlePath: String) -> String {
self.relayDir(bundlePath: bundlePath)
}
static func _testPreferredGatewayBind() -> String? {
self.preferredGatewayBind()
}
static func _testPreferredGatewayToken() -> String? {
self.preferredGatewayToken()
}
}
#endif

View File

@@ -204,3 +204,25 @@ final class MenuContextCardInjector: NSObject, NSMenuDelegate {
self.lastKnownMenuWidth = max(300, targetWidth)
}
}
#if DEBUG
extension MenuContextCardInjector {
func _testSetCache(rows: [SessionRow], errorText: String?, updatedAt: Date?) {
self.cachedRows = rows
self.cacheErrorText = errorText
self.cacheUpdatedAt = updatedAt
}
func _testFindInsertIndex(in menu: NSMenu) -> Int? {
self.findInsertIndex(in: menu)
}
func _testInitialCardWidth(for menu: NSMenu) -> CGFloat {
self.initialCardWidth(for: menu)
}
func _testCachedView() -> AnyView {
self.cachedView()
}
}
#endif

View File

@@ -153,58 +153,7 @@ actor PortGuardian {
for port in ports {
let listeners = await self.listeners(on: port)
let expectedDesc: String
let okPredicate: (Listener) -> Bool
let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"]
switch mode {
case .remote:
expectedDesc = "SSH tunnel to remote gateway"
okPredicate = { $0.command.lowercased().contains("ssh") }
case .local:
expectedDesc = "Gateway websocket (node/tsx)"
okPredicate = { listener in
let c = listener.command.lowercased()
return expectedCommands.contains { c.contains($0) }
}
case .unconfigured:
expectedDesc = "Gateway not configured"
okPredicate = { _ in false }
}
if listeners.isEmpty {
let text = "Nothing is listening on \(port) (\(expectedDesc))."
reports.append(.init(port: port, expected: expectedDesc, status: .missing(text), listeners: []))
continue
}
let reportListeners = listeners.map { listener in
ReportListener(
pid: listener.pid,
command: listener.command,
fullCommand: listener.fullCommand,
user: listener.user,
expected: okPredicate(listener))
}
let offenders = reportListeners.filter { !$0.expected }
if offenders.isEmpty {
let list = listeners.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ")
let okText = "Port \(port) is served by \(list)."
reports.append(.init(
port: port,
expected: expectedDesc,
status: .ok(okText),
listeners: reportListeners))
} else {
let list = offenders.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ")
let reason = "Port \(port) is held by \(list), expected \(expectedDesc)."
reports.append(.init(
port: port,
expected: expectedDesc,
status: .interference(reason, offenders: offenders),
listeners: reportListeners))
}
reports.append(Self.buildReport(port: port, listeners: listeners, mode: mode))
}
return reports
@@ -218,6 +167,29 @@ actor PortGuardian {
timeout: 5)
guard res.ok, let data = res.payload, !data.isEmpty else { return [] }
let text = String(data: data, encoding: .utf8) ?? ""
return Self.parseListeners(from: text)
}
private static func readFullCommand(pid: Int32) -> String? {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/bin/ps")
proc.arguments = ["-p", "\(pid)", "-o", "command="]
let pipe = Pipe()
proc.standardOutput = pipe
proc.standardError = Pipe()
do {
try proc.run()
proc.waitUntilExit()
} catch {
return nil
}
let data = pipe.fileHandleForReading.readToEndSafely()
guard !data.isEmpty else { return nil }
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func parseListeners(from text: String) -> [Listener] {
var listeners: [Listener] = []
var currentPid: Int32?
var currentCmd: String?
@@ -252,23 +224,62 @@ actor PortGuardian {
return listeners
}
private static func readFullCommand(pid: Int32) -> String? {
let proc = Process()
proc.executableURL = URL(fileURLWithPath: "/bin/ps")
proc.arguments = ["-p", "\(pid)", "-o", "command="]
let pipe = Pipe()
proc.standardOutput = pipe
proc.standardError = Pipe()
do {
try proc.run()
proc.waitUntilExit()
} catch {
return nil
private static func buildReport(
port: Int,
listeners: [Listener],
mode: AppState.ConnectionMode) -> PortReport
{
let expectedDesc: String
let okPredicate: (Listener) -> Bool
let expectedCommands = ["node", "clawdis", "tsx", "pnpm", "bun"]
switch mode {
case .remote:
expectedDesc = "SSH tunnel to remote gateway"
okPredicate = { $0.command.lowercased().contains("ssh") }
case .local:
expectedDesc = "Gateway websocket (node/tsx)"
okPredicate = { listener in
let c = listener.command.lowercased()
return expectedCommands.contains { c.contains($0) }
}
case .unconfigured:
expectedDesc = "Gateway not configured"
okPredicate = { _ in false }
}
let data = pipe.fileHandleForReading.readToEndSafely()
guard !data.isEmpty else { return nil }
return String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines)
if listeners.isEmpty {
let text = "Nothing is listening on \(port) (\(expectedDesc))."
return .init(port: port, expected: expectedDesc, status: .missing(text), listeners: [])
}
let reportListeners = listeners.map { listener in
ReportListener(
pid: listener.pid,
command: listener.command,
fullCommand: listener.fullCommand,
user: listener.user,
expected: okPredicate(listener))
}
let offenders = reportListeners.filter { !$0.expected }
if offenders.isEmpty {
let list = listeners.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ")
let okText = "Port \(port) is served by \(list)."
return .init(
port: port,
expected: expectedDesc,
status: .ok(okText),
listeners: reportListeners)
}
let list = offenders.map { "\($0.command) (\($0.pid))" }.joined(separator: ", ")
let reason = "Port \(port) is held by \(list), expected \(expectedDesc)."
return .init(
port: port,
expected: expectedDesc,
status: .interference(reason, offenders: offenders),
listeners: reportListeners)
}
private static func executablePath(for pid: Int32) -> String? {
@@ -319,3 +330,20 @@ actor PortGuardian {
try? data.write(to: Self.recordPath, options: [.atomic])
}
}
#if DEBUG
extension PortGuardian {
static func _testParseListeners(_ text: String) -> [(pid: Int32, command: String, fullCommand: String, user: String?)] {
Self.parseListeners(from: text).map { ($0.pid, $0.command, $0.fullCommand, $0.user) }
}
static func _testBuildReport(
port: Int,
mode: AppState.ConnectionMode,
listeners: [(pid: Int32, command: String, fullCommand: String, user: String?)]) -> PortReport
{
let mapped = listeners.map { Listener(pid: $0.pid, command: $0.command, fullCommand: $0.fullCommand, user: $0.user) }
return Self.buildReport(port: port, listeners: mapped, mode: mode)
}
}
#endif

View File

@@ -132,3 +132,27 @@ final class PresenceReporter {
return en0 ?? fallback
}
}
#if DEBUG
extension PresenceReporter {
static func _testComposePresenceSummary(mode: String, reason: String) -> String {
self.composePresenceSummary(mode: mode, reason: reason)
}
static func _testAppVersionString() -> String {
self.appVersionString()
}
static func _testPlatformString() -> String {
self.platformString()
}
static func _testLastInputSeconds() -> Int? {
self.lastInputSeconds()
}
static func _testPrimaryIPv4Address() -> String? {
self.primaryIPv4Address()
}
}
#endif

View File

@@ -17,3 +17,13 @@ extension View {
.onPreferenceChange(ViewWidthPreferenceKey.self, perform: onChange)
}
}
#if DEBUG
enum ViewMetricsTesting {
static func reduceWidth(current: CGFloat, next: CGFloat) -> CGFloat {
var value = current
ViewWidthPreferenceKey.reduce(value: &value, nextValue: { next })
return value
}
}
#endif

View File

@@ -510,4 +510,33 @@ struct VoiceWakeSettings_Previews: PreviewProvider {
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
}
}
@MainActor
extension VoiceWakeSettings {
static func exerciseForTesting() {
let state = AppState(preview: true)
state.swabbleEnabled = true
state.voicePushToTalkEnabled = true
state.swabbleTriggerWords = ["Claude", "Hey"]
var view = VoiceWakeSettings(state: state)
view.availableMics = [AudioInputDevice(uid: "mic-1", name: "Built-in")]
view.availableLocales = [Locale(identifier: "en_US")]
view.meterLevel = 0.42
view.meterError = "No input"
view.testState = .detected("ok")
view.isTesting = true
_ = view.body
_ = view.localePicker
_ = view.micPicker
_ = view.levelMeter
_ = view.triggerTable
_ = view.chimeSection
view.addWord()
_ = view.binding(for: 0).wrappedValue
view.removeWord(at: 0)
}
}
#endif