test: cover exec approval prompt gating
This commit is contained in:
@@ -475,8 +475,8 @@ enum ExecApprovalsStore {
|
|||||||
|
|
||||||
private static func mergeAgents(
|
private static func mergeAgents(
|
||||||
current: ExecApprovalsAgent,
|
current: ExecApprovalsAgent,
|
||||||
legacy: ExecApprovalsAgent
|
legacy: ExecApprovalsAgent) -> ExecApprovalsAgent
|
||||||
) -> ExecApprovalsAgent {
|
{
|
||||||
var seen = Set<String>()
|
var seen = Set<String>()
|
||||||
var allowlist: [ExecAllowlistEntry] = []
|
var allowlist: [ExecAllowlistEntry] = []
|
||||||
func append(_ entry: ExecAllowlistEntry) {
|
func append(_ entry: ExecAllowlistEntry) {
|
||||||
@@ -486,8 +486,12 @@ enum ExecApprovalsStore {
|
|||||||
seen.insert(key)
|
seen.insert(key)
|
||||||
allowlist.append(entry)
|
allowlist.append(entry)
|
||||||
}
|
}
|
||||||
for entry in current.allowlist ?? [] { append(entry) }
|
for entry in current.allowlist ?? [] {
|
||||||
for entry in legacy.allowlist ?? [] { append(entry) }
|
append(entry)
|
||||||
|
}
|
||||||
|
for entry in legacy.allowlist ?? [] {
|
||||||
|
append(entry)
|
||||||
|
}
|
||||||
|
|
||||||
return ExecApprovalsAgent(
|
return ExecApprovalsAgent(
|
||||||
security: current.security ?? legacy.security,
|
security: current.security ?? legacy.security,
|
||||||
|
|||||||
@@ -63,26 +63,38 @@ final class ExecApprovalsGatewayPrompter {
|
|||||||
let mode = AppStateStore.shared.connectionMode
|
let mode = AppStateStore.shared.connectionMode
|
||||||
let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let activeSession = WebChatManager.shared.activeSessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
let requestSession = request.request.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let recentlyActive = self.isRecentlyActive(mode: mode, thresholdSeconds: 120)
|
return Self.shouldPresent(
|
||||||
|
mode: mode,
|
||||||
|
activeSession: activeSession,
|
||||||
|
requestSession: requestSession,
|
||||||
|
lastInputSeconds: Self.lastInputSeconds(),
|
||||||
|
thresholdSeconds: 120)
|
||||||
|
}
|
||||||
|
|
||||||
if let session = requestSession, !session.isEmpty {
|
private static func shouldPresent(
|
||||||
if let active = activeSession, !active.isEmpty {
|
mode: AppState.ConnectionMode,
|
||||||
|
activeSession: String?,
|
||||||
|
requestSession: String?,
|
||||||
|
lastInputSeconds: Int?,
|
||||||
|
thresholdSeconds: Int) -> Bool
|
||||||
|
{
|
||||||
|
let active = activeSession?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let requested = requestSession?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let recentlyActive = lastInputSeconds.map { $0 <= thresholdSeconds } ?? (mode == .local)
|
||||||
|
|
||||||
|
if let session = requested, !session.isEmpty {
|
||||||
|
if let active, !active.isEmpty {
|
||||||
return active == session
|
return active == session
|
||||||
}
|
}
|
||||||
return recentlyActive
|
return recentlyActive
|
||||||
}
|
}
|
||||||
|
|
||||||
if let active = activeSession, !active.isEmpty {
|
if let active, !active.isEmpty {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return mode == .local
|
return mode == .local
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isRecentlyActive(mode: AppState.ConnectionMode, thresholdSeconds: Int) -> Bool {
|
|
||||||
guard let seconds = Self.lastInputSeconds() else { return mode == .local }
|
|
||||||
return seconds <= thresholdSeconds
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func lastInputSeconds() -> Int? {
|
private static func lastInputSeconds() -> Int? {
|
||||||
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
|
let anyEvent = CGEventType(rawValue: UInt32.max) ?? .null
|
||||||
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
|
let seconds = CGEventSource.secondsSinceLastEventType(.combinedSessionState, eventType: anyEvent)
|
||||||
@@ -90,3 +102,22 @@ final class ExecApprovalsGatewayPrompter {
|
|||||||
return Int(seconds.rounded())
|
return Int(seconds.rounded())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension ExecApprovalsGatewayPrompter {
|
||||||
|
static func _testShouldPresent(
|
||||||
|
mode: AppState.ConnectionMode,
|
||||||
|
activeSession: String?,
|
||||||
|
requestSession: String?,
|
||||||
|
lastInputSeconds: Int?,
|
||||||
|
thresholdSeconds: Int = 120) -> Bool
|
||||||
|
{
|
||||||
|
self.shouldPresent(
|
||||||
|
mode: mode,
|
||||||
|
activeSession: activeSession,
|
||||||
|
requestSession: requestSession,
|
||||||
|
lastInputSeconds: lastInputSeconds,
|
||||||
|
thresholdSeconds: thresholdSeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -560,13 +560,13 @@ actor GatewayEndpointStore {
|
|||||||
{
|
{
|
||||||
switch bindMode {
|
switch bindMode {
|
||||||
case "tailnet":
|
case "tailnet":
|
||||||
return tailscaleIP ?? "127.0.0.1"
|
tailscaleIP ?? "127.0.0.1"
|
||||||
case "auto":
|
case "auto":
|
||||||
return "127.0.0.1"
|
"127.0.0.1"
|
||||||
case "custom":
|
case "custom":
|
||||||
return customBindHost ?? "127.0.0.1"
|
customBindHost ?? "127.0.0.1"
|
||||||
default:
|
default:
|
||||||
return "127.0.0.1"
|
"127.0.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ extension MenuSessionsInjector {
|
|||||||
let headerItem = NSMenuItem()
|
let headerItem = NSMenuItem()
|
||||||
headerItem.tag = self.tag
|
headerItem.tag = self.tag
|
||||||
headerItem.isEnabled = false
|
headerItem.isEnabled = false
|
||||||
let statusText = self.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
|
let statusText = self
|
||||||
|
.cachedErrorText ?? (isConnected ? nil : self.controlChannelStatusText(for: channelState))
|
||||||
let hosted = self.makeHostedView(
|
let hosted = self.makeHostedView(
|
||||||
rootView: AnyView(MenuSessionsHeaderView(
|
rootView: AnyView(MenuSessionsHeaderView(
|
||||||
count: rows.count,
|
count: rows.count,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import Clawdbot
|
||||||
|
|
||||||
|
@Suite
|
||||||
|
@MainActor
|
||||||
|
struct ExecApprovalsGatewayPrompterTests {
|
||||||
|
@Test func sessionMatchPrefersActiveSession() {
|
||||||
|
let matches = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .remote,
|
||||||
|
activeSession: " main ",
|
||||||
|
requestSession: "main",
|
||||||
|
lastInputSeconds: nil)
|
||||||
|
#expect(matches)
|
||||||
|
|
||||||
|
let mismatched = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .remote,
|
||||||
|
activeSession: "other",
|
||||||
|
requestSession: "main",
|
||||||
|
lastInputSeconds: 0)
|
||||||
|
#expect(!mismatched)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func sessionFallbackUsesRecentActivity() {
|
||||||
|
let recent = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .remote,
|
||||||
|
activeSession: nil,
|
||||||
|
requestSession: "main",
|
||||||
|
lastInputSeconds: 10,
|
||||||
|
thresholdSeconds: 120)
|
||||||
|
#expect(recent)
|
||||||
|
|
||||||
|
let stale = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .remote,
|
||||||
|
activeSession: nil,
|
||||||
|
requestSession: "main",
|
||||||
|
lastInputSeconds: 200,
|
||||||
|
thresholdSeconds: 120)
|
||||||
|
#expect(!stale)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func defaultBehaviorMatchesMode() {
|
||||||
|
let local = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .local,
|
||||||
|
activeSession: nil,
|
||||||
|
requestSession: nil,
|
||||||
|
lastInputSeconds: 400)
|
||||||
|
#expect(local)
|
||||||
|
|
||||||
|
let remote = ExecApprovalsGatewayPrompter._testShouldPresent(
|
||||||
|
mode: .remote,
|
||||||
|
activeSession: nil,
|
||||||
|
requestSession: nil,
|
||||||
|
lastInputSeconds: 400)
|
||||||
|
#expect(!remote)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import ClawdbotKit
|
||||||
import Foundation
|
import Foundation
|
||||||
import os
|
import os
|
||||||
import Testing
|
import Testing
|
||||||
|
|||||||
Reference in New Issue
Block a user