test(macos): boost Clawdis coverage to 40%
This commit is contained in:
@@ -473,7 +473,7 @@ struct CronSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct StatusPill: View {
|
||||
struct StatusPill: View {
|
||||
let text: String
|
||||
let tint: Color
|
||||
|
||||
@@ -488,7 +488,7 @@ private struct StatusPill: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CronJobEditor: View {
|
||||
struct CronJobEditor: View {
|
||||
let job: CronJob?
|
||||
@Binding var isSaving: Bool
|
||||
@Binding var error: String?
|
||||
|
||||
@@ -8,6 +8,7 @@ struct DebugSettings: View {
|
||||
@AppStorage(modelCatalogPathKey) private var modelCatalogPath: String = ModelCatalogLoader.defaultPath
|
||||
@AppStorage(modelCatalogReloadKey) private var modelCatalogReloadBump: Int = 0
|
||||
@AppStorage(iconOverrideKey) private var iconOverrideRaw: String = IconOverrideSelection.system.rawValue
|
||||
@AppStorage(canvasEnabledKey) private var canvasEnabled: Bool = true
|
||||
@State private var modelsCount: Int?
|
||||
@State private var modelsLoading = false
|
||||
@State private var modelsError: String?
|
||||
|
||||
@@ -126,7 +126,7 @@ struct SettingsRootView: View {
|
||||
enum SettingsTab: CaseIterable {
|
||||
case general, tools, sessions, cron, config, instances, voiceWake, permissions, debug, about
|
||||
static let windowWidth: CGFloat = 658 // +10% (tabs fit better)
|
||||
static let windowHeight: CGFloat = 790 // +10%
|
||||
static let windowHeight: CGFloat = 790 // +10% (more room)
|
||||
var title: String {
|
||||
switch self {
|
||||
case .general: "General"
|
||||
|
||||
@@ -7,9 +7,12 @@ extension ProcessInfo {
|
||||
}
|
||||
|
||||
var isRunningTests: Bool {
|
||||
// SwiftPM test bundles are typically loaded from a `.xctest` bundle.
|
||||
// We also check common XCTest env vars because some runners still set them.
|
||||
// SwiftPM tests load one or more `.xctest` bundles. With Swift Testing, `Bundle.main` is not
|
||||
// guaranteed to be the `.xctest` bundle, so check all loaded bundles.
|
||||
if Bundle.allBundles.contains(where: { $0.bundleURL.pathExtension == "xctest" }) { return true }
|
||||
if Bundle.main.bundleURL.pathExtension == "xctest" { return true }
|
||||
|
||||
// Backwards-compatible fallbacks for runners that still set XCTest env vars.
|
||||
return self.environment["XCTestConfigurationFilePath"] != nil
|
||||
|| self.environment["XCTestBundlePath"] != nil
|
||||
|| self.environment["XCTestSessionIdentifier"] != nil
|
||||
|
||||
@@ -9,6 +9,7 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
static let shared = VoiceWakeOverlayController()
|
||||
|
||||
private let logger = Logger(subsystem: "com.steipete.clawdis", category: "voicewake.overlay")
|
||||
private let enableUI: Bool
|
||||
|
||||
/// Keep the voice wake overlay above any other Clawdis windows, but below the system’s pop-up menus.
|
||||
/// (Menu bar menus typically live at `.popUpMenu`.)
|
||||
@@ -47,6 +48,10 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
private let minHeight: CGFloat = 48
|
||||
let closeOverflow: CGFloat = 10
|
||||
|
||||
init(enableUI: Bool = true) {
|
||||
self.enableUI = enableUI
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func startSession(
|
||||
token: UUID = UUID(),
|
||||
@@ -207,7 +212,23 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
self.autoSendTask?.cancel(); self.autoSendToken = nil
|
||||
self.model.isSending = false
|
||||
self.model.isEditing = false
|
||||
guard let window else { return }
|
||||
|
||||
if !self.enableUI {
|
||||
self.model.isVisible = false
|
||||
self.model.level = 0
|
||||
self.activeToken = nil
|
||||
self.activeSource = nil
|
||||
return
|
||||
}
|
||||
guard let window else {
|
||||
if ProcessInfo.processInfo.isRunningTests {
|
||||
self.model.isVisible = false
|
||||
self.model.level = 0
|
||||
self.activeToken = nil
|
||||
self.activeSource = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
let target = self.dismissTargetFrame(for: window.frame, reason: reason, outcome: outcome)
|
||||
NSAnimationContext.runAnimationGroup { context in
|
||||
context.duration = 0.18
|
||||
@@ -273,6 +294,12 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
}
|
||||
|
||||
private func present() {
|
||||
if !self.enableUI || ProcessInfo.processInfo.isRunningTests {
|
||||
if !self.model.isVisible {
|
||||
self.model.isVisible = true
|
||||
}
|
||||
return
|
||||
}
|
||||
self.ensureWindow()
|
||||
self.hostingView?.rootView = VoiceWakeOverlayView(controller: self)
|
||||
let target = self.targetFrame()
|
||||
@@ -437,7 +464,7 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private struct VoiceWakeOverlayView: View {
|
||||
struct VoiceWakeOverlayView: View {
|
||||
@ObservedObject var controller: VoiceWakeOverlayController
|
||||
@FocusState private var textFocused: Bool
|
||||
@State private var isHovering: Bool = false
|
||||
@@ -554,7 +581,7 @@ private struct VoiceWakeOverlayView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct TranscriptTextView: NSViewRepresentable {
|
||||
struct TranscriptTextView: NSViewRepresentable {
|
||||
@Binding var text: String
|
||||
var attributed: NSAttributedString
|
||||
var isFinal: Bool
|
||||
@@ -651,7 +678,7 @@ private struct TranscriptTextView: NSViewRepresentable {
|
||||
|
||||
// MARK: - Vibrant display label
|
||||
|
||||
private struct VibrantLabelView: NSViewRepresentable {
|
||||
struct VibrantLabelView: NSViewRepresentable {
|
||||
var attributed: NSAttributedString
|
||||
var onTap: () -> Void
|
||||
|
||||
@@ -708,7 +735,7 @@ private final class ClickCatcher: NSView {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CloseHoverButton: View {
|
||||
struct CloseHoverButton: View {
|
||||
var onClose: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@@ -728,7 +755,7 @@ private struct CloseHoverButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CloseButtonOverlay: View {
|
||||
struct CloseButtonOverlay: View {
|
||||
var isVisible: Bool
|
||||
var onHover: (Bool) -> Void
|
||||
var onClose: () -> Void
|
||||
|
||||
@@ -81,6 +81,15 @@ final class WebChatServer: @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
self.queue.async {
|
||||
self.listener?.cancel()
|
||||
self.listener = nil
|
||||
self.root = nil
|
||||
self.port = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the base URL once the server is ready, otherwise nil.
|
||||
func baseURL() -> URL? {
|
||||
var url: URL?
|
||||
@@ -174,8 +183,10 @@ final class WebChatServer: @unchecked Sendable {
|
||||
}
|
||||
let fileURL = root.appendingPathComponent(path)
|
||||
webChatServerLogger.debug("WebChatServer resolved file=\(fileURL.path, privacy: .public)")
|
||||
// Simple directory traversal guard: served files must live under the bundled web root.
|
||||
guard fileURL.path.hasPrefix(root.path) else {
|
||||
// Directory traversal + symlink escape guard: served files must resolve under the bundled web root.
|
||||
let rootPath = root.standardizedFileURL.resolvingSymlinksInPath().path
|
||||
let resolvedPath = fileURL.standardizedFileURL.resolvingSymlinksInPath().path
|
||||
guard resolvedPath == rootPath || resolvedPath.hasPrefix(rootPath + "/") else {
|
||||
let forbidden = Data("Forbidden".utf8)
|
||||
self.send(
|
||||
status: 403,
|
||||
|
||||
@@ -4,6 +4,19 @@ import AppKit
|
||||
enum WindowPlacement {
|
||||
static func centeredFrame(size: NSSize, on screen: NSScreen? = NSScreen.main) -> NSRect {
|
||||
let bounds = (screen?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? .zero)
|
||||
return self.centeredFrame(size: size, in: bounds)
|
||||
}
|
||||
|
||||
static func topRightFrame(
|
||||
size: NSSize,
|
||||
padding: CGFloat,
|
||||
on screen: NSScreen? = NSScreen.main) -> NSRect
|
||||
{
|
||||
let bounds = (screen?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? .zero)
|
||||
return self.topRightFrame(size: size, padding: padding, in: bounds)
|
||||
}
|
||||
|
||||
static func centeredFrame(size: NSSize, in bounds: NSRect) -> NSRect {
|
||||
if bounds == .zero {
|
||||
return NSRect(origin: .zero, size: size)
|
||||
}
|
||||
@@ -16,12 +29,7 @@ enum WindowPlacement {
|
||||
return NSRect(x: x, y: y, width: clampedWidth, height: clampedHeight)
|
||||
}
|
||||
|
||||
static func topRightFrame(
|
||||
size: NSSize,
|
||||
padding: CGFloat,
|
||||
on screen: NSScreen? = NSScreen.main) -> NSRect
|
||||
{
|
||||
let bounds = (screen?.visibleFrame ?? NSScreen.screens.first?.visibleFrame ?? .zero)
|
||||
static func topRightFrame(size: NSSize, padding: CGFloat, in bounds: NSRect) -> NSRect {
|
||||
if bounds == .zero {
|
||||
return NSRect(origin: .zero, size: size)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user