test: expand settings coverage
This commit is contained in:
@@ -15,7 +15,14 @@ struct AnthropicAuthControls: View {
|
|||||||
@State private var autoConnectClipboard = true
|
@State private var autoConnectClipboard = true
|
||||||
@State private var lastPasteboardChangeCount = NSPasteboard.general.changeCount
|
@State private var lastPasteboardChangeCount = NSPasteboard.general.changeCount
|
||||||
|
|
||||||
private static let clipboardPoll = Timer.publish(every: 0.4, on: .main, in: .common).autoconnect()
|
private static let clipboardPoll: AnyPublisher<Date, Never> = {
|
||||||
|
if ProcessInfo.processInfo.isRunningTests {
|
||||||
|
return Empty(completeImmediately: false).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
return Timer.publish(every: 0.4, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
@@ -200,3 +207,28 @@ struct AnthropicAuthControls: View {
|
|||||||
Task { await self.finishOAuth() }
|
Task { await self.finishOAuth() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension AnthropicAuthControls {
|
||||||
|
init(
|
||||||
|
connectionMode: AppState.ConnectionMode,
|
||||||
|
oauthStatus: ClawdisOAuthStore.AnthropicOAuthStatus,
|
||||||
|
pkce: AnthropicOAuth.PKCE? = nil,
|
||||||
|
code: String = "",
|
||||||
|
busy: Bool = false,
|
||||||
|
statusText: String? = nil,
|
||||||
|
autoDetectClipboard: Bool = true,
|
||||||
|
autoConnectClipboard: Bool = true)
|
||||||
|
{
|
||||||
|
self.connectionMode = connectionMode
|
||||||
|
self._oauthStatus = State(initialValue: oauthStatus)
|
||||||
|
self._pkce = State(initialValue: pkce)
|
||||||
|
self._code = State(initialValue: code)
|
||||||
|
self._busy = State(initialValue: busy)
|
||||||
|
self._statusText = State(initialValue: statusText)
|
||||||
|
self._autoDetectClipboard = State(initialValue: autoDetectClipboard)
|
||||||
|
self._autoConnectClipboard = State(initialValue: autoConnectClipboard)
|
||||||
|
self._lastPasteboardChangeCount = State(initialValue: NSPasteboard.general.changeCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -1095,4 +1095,96 @@ struct CronSettings_Previews: PreviewProvider {
|
|||||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
extension CronSettings {
|
||||||
|
static func exerciseForTesting() {
|
||||||
|
let store = CronJobsStore(isPreview: true)
|
||||||
|
store.schedulerEnabled = false
|
||||||
|
store.schedulerStorePath = "/tmp/clawdis-cron-store.json"
|
||||||
|
|
||||||
|
let job = CronJob(
|
||||||
|
id: "job-1",
|
||||||
|
name: "Daily summary",
|
||||||
|
description: "Summary job",
|
||||||
|
enabled: true,
|
||||||
|
createdAtMs: 1_700_000_000_000,
|
||||||
|
updatedAtMs: 1_700_000_100_000,
|
||||||
|
schedule: .cron(expr: "0 8 * * *", tz: "UTC"),
|
||||||
|
sessionTarget: .isolated,
|
||||||
|
wakeMode: .nextHeartbeat,
|
||||||
|
payload: .agentTurn(
|
||||||
|
message: "Summarize",
|
||||||
|
thinking: "low",
|
||||||
|
timeoutSeconds: 120,
|
||||||
|
deliver: true,
|
||||||
|
channel: "whatsapp",
|
||||||
|
to: "+15551234567",
|
||||||
|
bestEffortDeliver: true),
|
||||||
|
isolation: CronIsolation(postToMainPrefix: "[cron] "),
|
||||||
|
state: CronJobState(
|
||||||
|
nextRunAtMs: 1_700_000_200_000,
|
||||||
|
runningAtMs: nil,
|
||||||
|
lastRunAtMs: 1_700_000_050_000,
|
||||||
|
lastStatus: "ok",
|
||||||
|
lastError: nil,
|
||||||
|
lastDurationMs: 1200))
|
||||||
|
|
||||||
|
let run = CronRunLogEntry(
|
||||||
|
ts: 1_700_000_050_000,
|
||||||
|
jobId: job.id,
|
||||||
|
action: "finished",
|
||||||
|
status: "ok",
|
||||||
|
error: nil,
|
||||||
|
summary: "done",
|
||||||
|
runAtMs: 1_700_000_050_000,
|
||||||
|
durationMs: 1200,
|
||||||
|
nextRunAtMs: 1_700_000_200_000)
|
||||||
|
|
||||||
|
store.jobs = [job]
|
||||||
|
store.selectedJobId = job.id
|
||||||
|
store.runEntries = [run]
|
||||||
|
|
||||||
|
var view = CronSettings(store: store)
|
||||||
|
_ = view.body
|
||||||
|
_ = view.jobRow(job)
|
||||||
|
_ = view.jobContextMenu(job)
|
||||||
|
_ = view.detailHeader(job)
|
||||||
|
_ = view.detailCard(job)
|
||||||
|
_ = view.runHistoryCard(job)
|
||||||
|
_ = view.runRow(run)
|
||||||
|
_ = view.payloadSummary(job.payload)
|
||||||
|
_ = view.scheduleSummary(job.schedule)
|
||||||
|
_ = view.statusTint(job.state.lastStatus)
|
||||||
|
_ = view.nextRunLabel(Date())
|
||||||
|
_ = view.formatDuration(ms: 1234)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CronJobEditor {
|
||||||
|
mutating func exerciseForTesting() {
|
||||||
|
self.name = "Test job"
|
||||||
|
self.description = "Test description"
|
||||||
|
self.enabled = true
|
||||||
|
self.sessionTarget = .isolated
|
||||||
|
self.wakeMode = .now
|
||||||
|
|
||||||
|
self.scheduleKind = .every
|
||||||
|
self.everyText = "15m"
|
||||||
|
|
||||||
|
self.payloadKind = .agentTurn
|
||||||
|
self.agentMessage = "Run diagnostic"
|
||||||
|
self.deliver = true
|
||||||
|
self.channel = .last
|
||||||
|
self.to = "+15551230000"
|
||||||
|
self.thinking = "low"
|
||||||
|
self.timeoutSeconds = "90"
|
||||||
|
self.bestEffortDeliver = true
|
||||||
|
self.postPrefix = "Cron"
|
||||||
|
|
||||||
|
_ = self.buildAgentTurnPayload()
|
||||||
|
_ = try? self.buildPayload()
|
||||||
|
_ = self.formatDuration(ms: 45000)
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -651,4 +651,42 @@ struct GeneralSettings_Previews: PreviewProvider {
|
|||||||
.environment(TailscaleService.shared)
|
.environment(TailscaleService.shared)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
extension GeneralSettings {
|
||||||
|
static func exerciseForTesting() {
|
||||||
|
let state = AppState(preview: true)
|
||||||
|
state.connectionMode = .remote
|
||||||
|
state.remoteTarget = "user@host:2222"
|
||||||
|
state.remoteIdentity = "/tmp/id_ed25519"
|
||||||
|
state.remoteProjectRoot = "/tmp/clawdis"
|
||||||
|
state.remoteCliPath = "/tmp/clawdis"
|
||||||
|
|
||||||
|
var view = GeneralSettings(state: state)
|
||||||
|
view.gatewayStatus = GatewayEnvironmentStatus(
|
||||||
|
kind: .ok,
|
||||||
|
nodeVersion: "1.0.0",
|
||||||
|
gatewayVersion: "1.0.0",
|
||||||
|
requiredGateway: nil,
|
||||||
|
message: "Gateway ready")
|
||||||
|
view.remoteStatus = .failed("SSH failed")
|
||||||
|
view.showRemoteAdvanced = true
|
||||||
|
view.cliInstalled = true
|
||||||
|
view.cliInstallLocation = "/usr/local/bin/clawdis"
|
||||||
|
view.cliStatus = "Installed"
|
||||||
|
_ = view.body
|
||||||
|
|
||||||
|
state.connectionMode = .unconfigured
|
||||||
|
_ = view.body
|
||||||
|
|
||||||
|
state.connectionMode = .local
|
||||||
|
view.gatewayStatus = GatewayEnvironmentStatus(
|
||||||
|
kind: .error("Gateway offline"),
|
||||||
|
nodeVersion: nil,
|
||||||
|
gatewayVersion: nil,
|
||||||
|
requiredGateway: nil,
|
||||||
|
message: "Gateway offline")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -346,6 +346,102 @@ struct InstancesSettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
extension InstancesSettings {
|
||||||
|
static func exerciseForTesting() {
|
||||||
|
let view = InstancesSettings(store: InstancesStore(isPreview: true))
|
||||||
|
let mac = InstanceInfo(
|
||||||
|
id: "mac",
|
||||||
|
host: "studio",
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
version: "1.2.3",
|
||||||
|
platform: "macOS 14.2",
|
||||||
|
deviceFamily: "Mac",
|
||||||
|
modelIdentifier: "Mac14,10",
|
||||||
|
lastInputSeconds: 12,
|
||||||
|
mode: "local",
|
||||||
|
reason: "self",
|
||||||
|
text: "Mac Studio",
|
||||||
|
ts: 1_700_000_000_000)
|
||||||
|
let genericIOS = InstanceInfo(
|
||||||
|
id: "iphone",
|
||||||
|
host: "phone",
|
||||||
|
ip: "10.0.0.3",
|
||||||
|
version: "2.0.0",
|
||||||
|
platform: "iOS 17.2",
|
||||||
|
deviceFamily: "iPhone",
|
||||||
|
modelIdentifier: nil,
|
||||||
|
lastInputSeconds: 35,
|
||||||
|
mode: "node",
|
||||||
|
reason: "connect",
|
||||||
|
text: "iPhone node",
|
||||||
|
ts: 1_700_000_100_000)
|
||||||
|
let android = InstanceInfo(
|
||||||
|
id: "android",
|
||||||
|
host: "pixel",
|
||||||
|
ip: nil,
|
||||||
|
version: "3.1.0",
|
||||||
|
platform: "Android 14",
|
||||||
|
deviceFamily: "Android",
|
||||||
|
modelIdentifier: nil,
|
||||||
|
lastInputSeconds: 90,
|
||||||
|
mode: "node",
|
||||||
|
reason: "seq gap",
|
||||||
|
text: "Android node",
|
||||||
|
ts: 1_700_000_200_000)
|
||||||
|
let gateway = InstanceInfo(
|
||||||
|
id: "gateway",
|
||||||
|
host: "gateway",
|
||||||
|
ip: "10.0.0.9",
|
||||||
|
version: "4.0.0",
|
||||||
|
platform: "Linux",
|
||||||
|
deviceFamily: nil,
|
||||||
|
modelIdentifier: nil,
|
||||||
|
lastInputSeconds: nil,
|
||||||
|
mode: "gateway",
|
||||||
|
reason: "periodic",
|
||||||
|
text: "Gateway",
|
||||||
|
ts: 1_700_000_300_000)
|
||||||
|
|
||||||
|
_ = view.instanceRow(mac)
|
||||||
|
_ = view.instanceRow(genericIOS)
|
||||||
|
_ = view.instanceRow(android)
|
||||||
|
_ = view.instanceRow(gateway)
|
||||||
|
|
||||||
|
_ = view.leadingDeviceSymbol(
|
||||||
|
mac,
|
||||||
|
device: DevicePresentation(title: "Mac Studio", symbol: "macstudio"))
|
||||||
|
_ = view.leadingDeviceSymbol(
|
||||||
|
mac,
|
||||||
|
device: DevicePresentation(title: "MacBook Pro", symbol: "laptopcomputer"))
|
||||||
|
_ = view.leadingDeviceSymbol(android, device: nil)
|
||||||
|
_ = view.platformIcon("tvOS 17.1")
|
||||||
|
_ = view.platformIcon("watchOS 10")
|
||||||
|
_ = view.platformIcon("unknown 1.0")
|
||||||
|
_ = view.prettyPlatform("macOS 14.2")
|
||||||
|
_ = view.prettyPlatform("iOS 17")
|
||||||
|
_ = view.prettyPlatform("ipados 17.1")
|
||||||
|
_ = view.prettyPlatform("linux")
|
||||||
|
_ = view.prettyPlatform(" ")
|
||||||
|
_ = view.parsePlatform("macOS 14.1")
|
||||||
|
_ = view.parsePlatform(" ")
|
||||||
|
_ = view.presenceUpdateSourceShortText("self")
|
||||||
|
_ = view.presenceUpdateSourceShortText("instances-refresh")
|
||||||
|
_ = view.presenceUpdateSourceShortText("seq gap")
|
||||||
|
_ = view.presenceUpdateSourceShortText("custom")
|
||||||
|
_ = view.presenceUpdateSourceShortText(" ")
|
||||||
|
_ = view.updateSummaryText(mac, isGateway: false)
|
||||||
|
_ = view.updateSummaryText(gateway, isGateway: true)
|
||||||
|
_ = view.presenceUpdateSourceHelp("")
|
||||||
|
_ = view.presenceUpdateSourceHelp("connect")
|
||||||
|
_ = view.safeSystemSymbol("not-a-symbol", fallback: "cpu")
|
||||||
|
_ = view.isSystemSymbolAvailable("sparkles")
|
||||||
|
_ = view.label(icon: "android", text: "Android")
|
||||||
|
_ = view.label(icon: "sparkles", text: "Sparkles")
|
||||||
|
_ = view.label(icon: nil, text: "Plain")
|
||||||
|
_ = AndroidMark().body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct InstancesSettings_Previews: PreviewProvider {
|
struct InstancesSettings_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
InstancesSettings(store: .preview())
|
InstancesSettings(store: .preview())
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ struct SkillsSettings: View {
|
|||||||
@State private var envEditor: EnvEditorState?
|
@State private var envEditor: EnvEditorState?
|
||||||
@State private var filter: SkillsFilter = .all
|
@State private var filter: SkillsFilter = .all
|
||||||
|
|
||||||
init(state: AppState = AppStateStore.shared) {
|
init(state: AppState = AppStateStore.shared, model: SkillsSettingsModel = SkillsSettingsModel()) {
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self._model = State(initialValue: model)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -571,4 +572,55 @@ struct SkillsSettings_Previews: PreviewProvider {
|
|||||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SkillsSettings {
|
||||||
|
static func exerciseForTesting() {
|
||||||
|
let skill = SkillStatus(
|
||||||
|
name: "Test Skill",
|
||||||
|
description: "Test description",
|
||||||
|
source: "clawdis-bundled",
|
||||||
|
filePath: "/tmp/skills/test",
|
||||||
|
baseDir: "/tmp/skills",
|
||||||
|
skillKey: "test",
|
||||||
|
primaryEnv: "API_KEY",
|
||||||
|
emoji: "🧪",
|
||||||
|
homepage: "https://example.com",
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: false,
|
||||||
|
requirements: SkillRequirements(bins: ["python3"], env: ["API_KEY"], config: ["skills.test"]),
|
||||||
|
missing: SkillMissing(bins: ["python3"], env: ["API_KEY"], config: ["skills.test"]),
|
||||||
|
configChecks: [
|
||||||
|
SkillStatusConfigCheck(path: "skills.test", value: AnyCodable(false), satisfied: false),
|
||||||
|
],
|
||||||
|
install: [
|
||||||
|
SkillInstallOption(id: "brew", kind: "brew", label: "brew install python", bins: ["python3"]),
|
||||||
|
])
|
||||||
|
|
||||||
|
let row = SkillRow(
|
||||||
|
skill: skill,
|
||||||
|
isBusy: false,
|
||||||
|
connectionMode: .remote,
|
||||||
|
onToggleEnabled: { _ in },
|
||||||
|
onInstall: { _, _ in },
|
||||||
|
onSetEnv: { _, _ in })
|
||||||
|
_ = row.body
|
||||||
|
|
||||||
|
_ = SkillTag(text: "Bundled").body
|
||||||
|
|
||||||
|
let editor = EnvEditorView(
|
||||||
|
editor: EnvEditorState(
|
||||||
|
skillKey: "test",
|
||||||
|
skillName: "Test Skill",
|
||||||
|
envKey: "API_KEY",
|
||||||
|
isPrimary: true),
|
||||||
|
onSave: { _ in })
|
||||||
|
_ = editor.body
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func setFilterForTesting(_ rawValue: String) {
|
||||||
|
guard let filter = SkillsFilter(rawValue: rawValue) else { return }
|
||||||
|
self.filter = filter
|
||||||
|
}
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ struct TailscaleIntegrationSection: View {
|
|||||||
let isPaused: Bool
|
let isPaused: Bool
|
||||||
|
|
||||||
@Environment(TailscaleService.self) private var tailscaleService
|
@Environment(TailscaleService.self) private var tailscaleService
|
||||||
|
#if DEBUG
|
||||||
|
private var testingService: TailscaleService?
|
||||||
|
#endif
|
||||||
|
|
||||||
@State private var hasLoaded = false
|
@State private var hasLoaded = false
|
||||||
@State private var tailscaleMode: GatewayTailscaleMode = .off
|
@State private var tailscaleMode: GatewayTailscaleMode = .off
|
||||||
@@ -41,6 +44,22 @@ struct TailscaleIntegrationSection: View {
|
|||||||
@State private var validationMessage: String?
|
@State private var validationMessage: String?
|
||||||
@State private var statusTimer: Timer?
|
@State private var statusTimer: Timer?
|
||||||
|
|
||||||
|
init(connectionMode: AppState.ConnectionMode, isPaused: Bool) {
|
||||||
|
self.connectionMode = connectionMode
|
||||||
|
self.isPaused = isPaused
|
||||||
|
#if DEBUG
|
||||||
|
self.testingService = nil
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var effectiveService: TailscaleService {
|
||||||
|
#if DEBUG
|
||||||
|
return self.testingService ?? self.tailscaleService
|
||||||
|
#else
|
||||||
|
return self.tailscaleService
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
Text("Tailscale (dashboard access)")
|
Text("Tailscale (dashboard access)")
|
||||||
@@ -48,7 +67,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
|
|
||||||
self.statusRow
|
self.statusRow
|
||||||
|
|
||||||
if !self.tailscaleService.isInstalled {
|
if !self.effectiveService.isInstalled {
|
||||||
self.installButtons
|
self.installButtons
|
||||||
} else {
|
} else {
|
||||||
self.modePicker
|
self.modePicker
|
||||||
@@ -87,7 +106,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
guard !self.hasLoaded else { return }
|
guard !self.hasLoaded else { return }
|
||||||
self.hasLoaded = true
|
self.hasLoaded = true
|
||||||
self.loadConfig()
|
self.loadConfig()
|
||||||
await self.tailscaleService.checkTailscaleStatus()
|
await self.effectiveService.checkTailscaleStatus()
|
||||||
self.startStatusTimer()
|
self.startStatusTimer()
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
@@ -110,7 +129,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
.font(.callout)
|
.font(.callout)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Refresh") {
|
Button("Refresh") {
|
||||||
Task { await self.tailscaleService.checkTailscaleStatus() }
|
Task { await self.effectiveService.checkTailscaleStatus() }
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -118,24 +137,24 @@ struct TailscaleIntegrationSection: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var statusColor: Color {
|
private var statusColor: Color {
|
||||||
if !self.tailscaleService.isInstalled { return .yellow }
|
if !self.effectiveService.isInstalled { return .yellow }
|
||||||
if self.tailscaleService.isRunning { return .green }
|
if self.effectiveService.isRunning { return .green }
|
||||||
return .orange
|
return .orange
|
||||||
}
|
}
|
||||||
|
|
||||||
private var statusText: String {
|
private var statusText: String {
|
||||||
if !self.tailscaleService.isInstalled { return "Tailscale is not installed" }
|
if !self.effectiveService.isInstalled { return "Tailscale is not installed" }
|
||||||
if self.tailscaleService.isRunning { return "Tailscale is installed and running" }
|
if self.effectiveService.isRunning { return "Tailscale is installed and running" }
|
||||||
return "Tailscale is installed but not running"
|
return "Tailscale is installed but not running"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var installButtons: some View {
|
private var installButtons: some View {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
Button("App Store") { self.tailscaleService.openAppStore() }
|
Button("App Store") { self.effectiveService.openAppStore() }
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
Button("Direct Download") { self.tailscaleService.openDownloadPage() }
|
Button("Direct Download") { self.effectiveService.openDownloadPage() }
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
Button("Setup Guide") { self.tailscaleService.openSetupGuide() }
|
Button("Setup Guide") { self.effectiveService.openSetupGuide() }
|
||||||
.buttonStyle(.link)
|
.buttonStyle(.link)
|
||||||
}
|
}
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
@@ -159,7 +178,7 @@ struct TailscaleIntegrationSection: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var accessURLRow: some View {
|
private var accessURLRow: some View {
|
||||||
if let host = self.tailscaleService.tailscaleHostname {
|
if let host = self.effectiveService.tailscaleHostname {
|
||||||
let url = "https://\(host)/ui/"
|
let url = "https://\(host)/ui/"
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text("Dashboard URL:")
|
Text("Dashboard URL:")
|
||||||
@@ -173,14 +192,14 @@ struct TailscaleIntegrationSection: View {
|
|||||||
.font(.system(.caption, design: .monospaced))
|
.font(.system(.caption, design: .monospaced))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if !self.tailscaleService.isRunning {
|
} else if !self.effectiveService.isRunning {
|
||||||
Text("Start Tailscale to get your tailnet hostname.")
|
Text("Start Tailscale to get your tailnet hostname.")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tailscaleService.isInstalled, !self.tailscaleService.isRunning {
|
if self.effectiveService.isInstalled, !self.effectiveService.isRunning {
|
||||||
Button("Start Tailscale") { self.tailscaleService.openTailscaleApp() }
|
Button("Start Tailscale") { self.effectiveService.openTailscaleApp() }
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.controlSize(.small)
|
.controlSize(.small)
|
||||||
}
|
}
|
||||||
@@ -304,8 +323,11 @@ struct TailscaleIntegrationSection: View {
|
|||||||
|
|
||||||
private func startStatusTimer() {
|
private func startStatusTimer() {
|
||||||
self.stopStatusTimer()
|
self.stopStatusTimer()
|
||||||
|
if ProcessInfo.processInfo.isRunningTests {
|
||||||
|
return
|
||||||
|
}
|
||||||
self.statusTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
|
self.statusTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
|
||||||
Task { await self.tailscaleService.checkTailscaleStatus() }
|
Task { await self.effectiveService.checkTailscaleStatus() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,3 +336,27 @@ struct TailscaleIntegrationSection: View {
|
|||||||
self.statusTimer = nil
|
self.statusTimer = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
extension TailscaleIntegrationSection {
|
||||||
|
mutating func setTestingState(
|
||||||
|
mode: String,
|
||||||
|
requireCredentials: Bool,
|
||||||
|
password: String = "secret",
|
||||||
|
statusMessage: String? = nil,
|
||||||
|
validationMessage: String? = nil)
|
||||||
|
{
|
||||||
|
if let mode = GatewayTailscaleMode(rawValue: mode) {
|
||||||
|
self.tailscaleMode = mode
|
||||||
|
}
|
||||||
|
self.requireCredentialsForServe = requireCredentials
|
||||||
|
self.password = password
|
||||||
|
self.statusMessage = statusMessage
|
||||||
|
self.validationMessage = validationMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func setTestingService(_ service: TailscaleService?) {
|
||||||
|
self.testingService = service
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ final class TailscaleService {
|
|||||||
Task { await self.checkTailscaleStatus() }
|
Task { await self.checkTailscaleStatus() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
init(
|
||||||
|
isInstalled: Bool,
|
||||||
|
isRunning: Bool,
|
||||||
|
tailscaleHostname: String? = nil,
|
||||||
|
tailscaleIP: String? = nil,
|
||||||
|
statusError: String? = nil)
|
||||||
|
{
|
||||||
|
self.isInstalled = isInstalled
|
||||||
|
self.isRunning = isRunning
|
||||||
|
self.tailscaleHostname = tailscaleHostname
|
||||||
|
self.tailscaleIP = tailscaleIP
|
||||||
|
self.statusError = statusError
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
func checkAppInstallation() -> Bool {
|
func checkAppInstallation() -> Bool {
|
||||||
let installed = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app")
|
let installed = FileManager.default.fileExists(atPath: "/Applications/Tailscale.app")
|
||||||
self.logger.info("Tailscale app installed: \(installed)")
|
self.logger.info("Tailscale app installed: \(installed)")
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import Clawdis
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
@MainActor
|
||||||
|
struct AnthropicAuthControlsSmokeTests {
|
||||||
|
@Test func anthropicAuthControlsBuildsBodyLocal() {
|
||||||
|
let pkce = AnthropicOAuth.PKCE(verifier: "verifier", challenge: "challenge")
|
||||||
|
let view = AnthropicAuthControls(
|
||||||
|
connectionMode: .local,
|
||||||
|
oauthStatus: .connected(expiresAtMs: 1_700_000_000_000),
|
||||||
|
pkce: pkce,
|
||||||
|
code: "code#state",
|
||||||
|
statusText: "Detected code",
|
||||||
|
autoDetectClipboard: false,
|
||||||
|
autoConnectClipboard: false)
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func anthropicAuthControlsBuildsBodyRemote() {
|
||||||
|
let view = AnthropicAuthControls(
|
||||||
|
connectionMode: .remote,
|
||||||
|
oauthStatus: .missingFile,
|
||||||
|
pkce: nil,
|
||||||
|
code: "",
|
||||||
|
statusText: nil)
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,21 +6,9 @@ import Testing
|
|||||||
struct AnthropicAuthResolverTests {
|
struct AnthropicAuthResolverTests {
|
||||||
@Test
|
@Test
|
||||||
func prefersOAuthFileOverEnv() throws {
|
func prefersOAuthFileOverEnv() throws {
|
||||||
let key = "CLAWDIS_OAUTH_DIR"
|
|
||||||
let previous = ProcessInfo.processInfo.environment[key]
|
|
||||||
defer {
|
|
||||||
if let previous {
|
|
||||||
setenv(key, previous, 1)
|
|
||||||
} else {
|
|
||||||
unsetenv(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir = FileManager.default.temporaryDirectory
|
let dir = FileManager.default.temporaryDirectory
|
||||||
.appendingPathComponent("clawdis-oauth-\(UUID().uuidString)", isDirectory: true)
|
.appendingPathComponent("clawdis-oauth-\(UUID().uuidString)", isDirectory: true)
|
||||||
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||||
setenv(key, dir.path, 1)
|
|
||||||
|
|
||||||
let oauthFile = dir.appendingPathComponent("oauth.json")
|
let oauthFile = dir.appendingPathComponent("oauth.json")
|
||||||
let payload = [
|
let payload = [
|
||||||
"anthropic": [
|
"anthropic": [
|
||||||
@@ -33,9 +21,10 @@ struct AnthropicAuthResolverTests {
|
|||||||
let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys])
|
let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted, .sortedKeys])
|
||||||
try data.write(to: oauthFile, options: [.atomic])
|
try data.write(to: oauthFile, options: [.atomic])
|
||||||
|
|
||||||
|
let status = ClawdisOAuthStore.anthropicOAuthStatus(at: oauthFile)
|
||||||
let mode = AnthropicAuthResolver.resolve(environment: [
|
let mode = AnthropicAuthResolver.resolve(environment: [
|
||||||
"ANTHROPIC_API_KEY": "sk-ant-ignored",
|
"ANTHROPIC_API_KEY": "sk-ant-ignored",
|
||||||
])
|
], oauthStatus: status)
|
||||||
#expect(mode == .oauthFile)
|
#expect(mode == .oauthFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Testing
|
||||||
|
@testable import Clawdis
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
@MainActor
|
||||||
|
struct ConnectionsSettingsSmokeTests {
|
||||||
|
@Test func connectionsSettingsBuildsBodyWithSnapshot() {
|
||||||
|
let store = ConnectionsStore(isPreview: true)
|
||||||
|
store.snapshot = ProvidersStatusSnapshot(
|
||||||
|
ts: 1_700_000_000_000,
|
||||||
|
whatsapp: ProvidersStatusSnapshot.WhatsAppStatus(
|
||||||
|
configured: true,
|
||||||
|
linked: true,
|
||||||
|
authAgeMs: 86_400_000,
|
||||||
|
self: ProvidersStatusSnapshot.WhatsAppSelf(
|
||||||
|
e164: "+15551234567",
|
||||||
|
jid: nil),
|
||||||
|
running: true,
|
||||||
|
connected: false,
|
||||||
|
lastConnectedAt: 1_700_000_000_000,
|
||||||
|
lastDisconnect: ProvidersStatusSnapshot.WhatsAppDisconnect(
|
||||||
|
at: 1_700_000_050_000,
|
||||||
|
status: 401,
|
||||||
|
error: "logged out",
|
||||||
|
loggedOut: true),
|
||||||
|
reconnectAttempts: 2,
|
||||||
|
lastMessageAt: 1_700_000_060_000,
|
||||||
|
lastEventAt: 1_700_000_060_000,
|
||||||
|
lastError: "needs login"),
|
||||||
|
telegram: ProvidersStatusSnapshot.TelegramStatus(
|
||||||
|
configured: true,
|
||||||
|
tokenSource: "env",
|
||||||
|
running: true,
|
||||||
|
mode: "polling",
|
||||||
|
lastStartAt: 1_700_000_000_000,
|
||||||
|
lastStopAt: nil,
|
||||||
|
lastError: nil,
|
||||||
|
probe: ProvidersStatusSnapshot.TelegramProbe(
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
error: nil,
|
||||||
|
elapsedMs: 120,
|
||||||
|
bot: ProvidersStatusSnapshot.TelegramBot(id: 123, username: "clawdisbot"),
|
||||||
|
webhook: ProvidersStatusSnapshot.TelegramWebhook(url: "https://example.com/hook", hasCustomCert: false)),
|
||||||
|
lastProbeAt: 1_700_000_050_000))
|
||||||
|
|
||||||
|
store.whatsappLoginMessage = "Scan QR"
|
||||||
|
store.whatsappLoginQrDataUrl =
|
||||||
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMB/ay7pS8AAAAASUVORK5CYII="
|
||||||
|
store.telegramToken = "123:abc"
|
||||||
|
store.telegramRequireMention = false
|
||||||
|
store.telegramAllowFrom = "123456789"
|
||||||
|
store.telegramProxy = "socks5://localhost:9050"
|
||||||
|
store.telegramWebhookUrl = "https://example.com/telegram"
|
||||||
|
store.telegramWebhookSecret = "secret"
|
||||||
|
store.telegramWebhookPath = "/telegram"
|
||||||
|
|
||||||
|
let view = ConnectionsSettings(store: store)
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func connectionsSettingsBuildsBodyWithoutSnapshot() {
|
||||||
|
let store = ConnectionsStore(isPreview: true)
|
||||||
|
store.snapshot = ProvidersStatusSnapshot(
|
||||||
|
ts: 1_700_000_000_000,
|
||||||
|
whatsapp: ProvidersStatusSnapshot.WhatsAppStatus(
|
||||||
|
configured: false,
|
||||||
|
linked: false,
|
||||||
|
authAgeMs: nil,
|
||||||
|
self: nil,
|
||||||
|
running: false,
|
||||||
|
connected: false,
|
||||||
|
lastConnectedAt: nil,
|
||||||
|
lastDisconnect: nil,
|
||||||
|
reconnectAttempts: 0,
|
||||||
|
lastMessageAt: nil,
|
||||||
|
lastEventAt: nil,
|
||||||
|
lastError: nil),
|
||||||
|
telegram: ProvidersStatusSnapshot.TelegramStatus(
|
||||||
|
configured: false,
|
||||||
|
tokenSource: nil,
|
||||||
|
running: false,
|
||||||
|
mode: nil,
|
||||||
|
lastStartAt: nil,
|
||||||
|
lastStopAt: nil,
|
||||||
|
lastError: "bot missing",
|
||||||
|
probe: ProvidersStatusSnapshot.TelegramProbe(
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
error: "unauthorized",
|
||||||
|
elapsedMs: 120,
|
||||||
|
bot: nil,
|
||||||
|
webhook: nil),
|
||||||
|
lastProbeAt: 1_700_000_100_000))
|
||||||
|
|
||||||
|
let view = ConnectionsSettings(store: store)
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,5 +55,14 @@ struct CronJobEditorSmokeTests {
|
|||||||
onSave: { _ in })
|
onSave: { _ in })
|
||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
@Test func cronJobEditorExercisesBuilders() {
|
||||||
|
var view = CronJobEditor(
|
||||||
|
job: nil,
|
||||||
|
isSaving: .constant(false),
|
||||||
|
error: .constant(nil),
|
||||||
|
onCancel: {},
|
||||||
|
onSave: { _ in })
|
||||||
|
view.exerciseForTesting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import Clawdis
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
@MainActor
|
||||||
|
struct InstancesSettingsSmokeTests {
|
||||||
|
@Test func instancesSettingsBuildsBodyWithMultipleInstances() {
|
||||||
|
let store = InstancesStore(isPreview: true)
|
||||||
|
store.statusMessage = "Loaded"
|
||||||
|
store.instances = [
|
||||||
|
InstanceInfo(
|
||||||
|
id: "macbook",
|
||||||
|
host: "macbook-pro",
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
version: "1.2.3",
|
||||||
|
platform: "macOS 15.1",
|
||||||
|
deviceFamily: "Mac",
|
||||||
|
modelIdentifier: "MacBookPro18,1",
|
||||||
|
lastInputSeconds: 15,
|
||||||
|
mode: "local",
|
||||||
|
reason: "heartbeat",
|
||||||
|
text: "MacBook Pro local",
|
||||||
|
ts: 1_700_000_000_000),
|
||||||
|
InstanceInfo(
|
||||||
|
id: "android",
|
||||||
|
host: "pixel",
|
||||||
|
ip: "10.0.0.3",
|
||||||
|
version: "2.0.0",
|
||||||
|
platform: "Android 14",
|
||||||
|
deviceFamily: "Android",
|
||||||
|
modelIdentifier: nil,
|
||||||
|
lastInputSeconds: 120,
|
||||||
|
mode: "node",
|
||||||
|
reason: "presence",
|
||||||
|
text: "Android node",
|
||||||
|
ts: 1_700_000_100_000),
|
||||||
|
InstanceInfo(
|
||||||
|
id: "gateway",
|
||||||
|
host: "gateway",
|
||||||
|
ip: "10.0.0.4",
|
||||||
|
version: "3.0.0",
|
||||||
|
platform: "iOS 17",
|
||||||
|
deviceFamily: nil,
|
||||||
|
modelIdentifier: nil,
|
||||||
|
lastInputSeconds: nil,
|
||||||
|
mode: "gateway",
|
||||||
|
reason: "gateway",
|
||||||
|
text: "Gateway",
|
||||||
|
ts: 1_700_000_200_000),
|
||||||
|
]
|
||||||
|
|
||||||
|
let view = InstancesSettings(store: store)
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func instancesSettingsExercisesHelpers() {
|
||||||
|
InstancesSettings.exerciseForTesting()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ struct SettingsViewSmokeTests {
|
|||||||
let job1 = CronJob(
|
let job1 = CronJob(
|
||||||
id: "job-1",
|
id: "job-1",
|
||||||
name: " Morning Check-in ",
|
name: " Morning Check-in ",
|
||||||
|
description: nil,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
createdAtMs: 1_700_000_000_000,
|
createdAtMs: 1_700_000_000_000,
|
||||||
updatedAtMs: 1_700_000_100_000,
|
updatedAtMs: 1_700_000_100_000,
|
||||||
@@ -31,7 +32,8 @@ struct SettingsViewSmokeTests {
|
|||||||
|
|
||||||
let job2 = CronJob(
|
let job2 = CronJob(
|
||||||
id: "job-2",
|
id: "job-2",
|
||||||
name: nil,
|
name: "",
|
||||||
|
description: nil,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
createdAtMs: 1_700_000_000_000,
|
createdAtMs: 1_700_000_000_000,
|
||||||
updatedAtMs: 1_700_000_100_000,
|
updatedAtMs: 1_700_000_100_000,
|
||||||
@@ -74,6 +76,10 @@ struct SettingsViewSmokeTests {
|
|||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func cronSettingsExercisesPrivateViews() {
|
||||||
|
CronSettings.exerciseForTesting()
|
||||||
|
}
|
||||||
|
|
||||||
@Test func configSettingsBuildsBody() {
|
@Test func configSettingsBuildsBody() {
|
||||||
let view = ConfigSettings()
|
let view = ConfigSettings()
|
||||||
_ = view.body
|
_ = view.body
|
||||||
@@ -90,6 +96,10 @@ struct SettingsViewSmokeTests {
|
|||||||
_ = view.body
|
_ = view.body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func generalSettingsExercisesBranches() {
|
||||||
|
GeneralSettings.exerciseForTesting()
|
||||||
|
}
|
||||||
|
|
||||||
@Test func sessionsSettingsBuildsBody() {
|
@Test func sessionsSettingsBuildsBody() {
|
||||||
let view = SessionsSettings(rows: SessionRow.previewRows, isPreview: true)
|
let view = SessionsSettings(rows: SessionRow.previewRows, isPreview: true)
|
||||||
_ = view.body
|
_ = view.body
|
||||||
|
|||||||
118
apps/macos/Tests/ClawdisIPCTests/SkillsSettingsSmokeTests.swift
Normal file
118
apps/macos/Tests/ClawdisIPCTests/SkillsSettingsSmokeTests.swift
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import Testing
|
||||||
|
@testable import Clawdis
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
@MainActor
|
||||||
|
struct SkillsSettingsSmokeTests {
|
||||||
|
@Test func skillsSettingsBuildsBodyWithSkillsRemote() {
|
||||||
|
let model = SkillsSettingsModel()
|
||||||
|
model.statusMessage = "Loaded"
|
||||||
|
model.skills = [
|
||||||
|
SkillStatus(
|
||||||
|
name: "Needs Setup",
|
||||||
|
description: "Missing bins and env",
|
||||||
|
source: "clawdis-managed",
|
||||||
|
filePath: "/tmp/skills/needs-setup",
|
||||||
|
baseDir: "/tmp/skills",
|
||||||
|
skillKey: "needs-setup",
|
||||||
|
primaryEnv: "API_KEY",
|
||||||
|
emoji: "🧰",
|
||||||
|
homepage: "https://example.com/needs-setup",
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: false,
|
||||||
|
requirements: SkillRequirements(
|
||||||
|
bins: ["python3"],
|
||||||
|
env: ["API_KEY"],
|
||||||
|
config: ["skills.needs-setup"]),
|
||||||
|
missing: SkillMissing(
|
||||||
|
bins: ["python3"],
|
||||||
|
env: ["API_KEY"],
|
||||||
|
config: ["skills.needs-setup"]),
|
||||||
|
configChecks: [
|
||||||
|
SkillStatusConfigCheck(path: "skills.needs-setup", value: AnyCodable(false), satisfied: false),
|
||||||
|
],
|
||||||
|
install: [
|
||||||
|
SkillInstallOption(id: "brew", kind: "brew", label: "brew install python", bins: ["python3"]),
|
||||||
|
]),
|
||||||
|
SkillStatus(
|
||||||
|
name: "Ready Skill",
|
||||||
|
description: "All set",
|
||||||
|
source: "clawdis-bundled",
|
||||||
|
filePath: "/tmp/skills/ready",
|
||||||
|
baseDir: "/tmp/skills",
|
||||||
|
skillKey: "ready",
|
||||||
|
primaryEnv: nil,
|
||||||
|
emoji: "✅",
|
||||||
|
homepage: "https://example.com/ready",
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: true,
|
||||||
|
requirements: SkillRequirements(bins: [], env: [], config: []),
|
||||||
|
missing: SkillMissing(bins: [], env: [], config: []),
|
||||||
|
configChecks: [
|
||||||
|
SkillStatusConfigCheck(path: "skills.ready", value: AnyCodable(true), satisfied: true),
|
||||||
|
SkillStatusConfigCheck(path: "skills.limit", value: AnyCodable(5), satisfied: true),
|
||||||
|
],
|
||||||
|
install: []),
|
||||||
|
SkillStatus(
|
||||||
|
name: "Disabled Skill",
|
||||||
|
description: "Disabled in config",
|
||||||
|
source: "clawdis-extra",
|
||||||
|
filePath: "/tmp/skills/disabled",
|
||||||
|
baseDir: "/tmp/skills",
|
||||||
|
skillKey: "disabled",
|
||||||
|
primaryEnv: nil,
|
||||||
|
emoji: "🚫",
|
||||||
|
homepage: nil,
|
||||||
|
always: false,
|
||||||
|
disabled: true,
|
||||||
|
eligible: false,
|
||||||
|
requirements: SkillRequirements(bins: [], env: [], config: []),
|
||||||
|
missing: SkillMissing(bins: [], env: [], config: []),
|
||||||
|
configChecks: [],
|
||||||
|
install: []),
|
||||||
|
]
|
||||||
|
|
||||||
|
let state = AppState(preview: true)
|
||||||
|
state.connectionMode = .remote
|
||||||
|
var view = SkillsSettings(state: state, model: model)
|
||||||
|
view.setFilterForTesting("all")
|
||||||
|
_ = view.body
|
||||||
|
view.setFilterForTesting("needsSetup")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func skillsSettingsBuildsBodyWithLocalMode() {
|
||||||
|
let model = SkillsSettingsModel()
|
||||||
|
model.skills = [
|
||||||
|
SkillStatus(
|
||||||
|
name: "Local Skill",
|
||||||
|
description: "Local ready",
|
||||||
|
source: "clawdis-workspace",
|
||||||
|
filePath: "/tmp/skills/local",
|
||||||
|
baseDir: "/tmp/skills",
|
||||||
|
skillKey: "local",
|
||||||
|
primaryEnv: nil,
|
||||||
|
emoji: "🏠",
|
||||||
|
homepage: nil,
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
eligible: true,
|
||||||
|
requirements: SkillRequirements(bins: [], env: [], config: []),
|
||||||
|
missing: SkillMissing(bins: [], env: [], config: []),
|
||||||
|
configChecks: [],
|
||||||
|
install: []),
|
||||||
|
]
|
||||||
|
|
||||||
|
let state = AppState(preview: true)
|
||||||
|
state.connectionMode = .local
|
||||||
|
var view = SkillsSettings(state: state, model: model)
|
||||||
|
view.setFilterForTesting("ready")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func skillsSettingsExercisesPrivateViews() {
|
||||||
|
SkillsSettings.exerciseForTesting()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Testing
|
||||||
|
@testable import Clawdis
|
||||||
|
|
||||||
|
@Suite(.serialized)
|
||||||
|
@MainActor
|
||||||
|
struct TailscaleIntegrationSectionTests {
|
||||||
|
@Test func tailscaleSectionBuildsBodyWhenNotInstalled() {
|
||||||
|
let service = TailscaleService(isInstalled: false, isRunning: false, statusError: "not installed")
|
||||||
|
var view = TailscaleIntegrationSection(connectionMode: .local, isPaused: false)
|
||||||
|
view.setTestingService(service)
|
||||||
|
view.setTestingState(mode: "off", requireCredentials: false, statusMessage: "Idle")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func tailscaleSectionBuildsBodyForServeMode() {
|
||||||
|
let service = TailscaleService(
|
||||||
|
isInstalled: true,
|
||||||
|
isRunning: true,
|
||||||
|
tailscaleHostname: "clawdis.tailnet.ts.net",
|
||||||
|
tailscaleIP: "100.64.0.1")
|
||||||
|
var view = TailscaleIntegrationSection(connectionMode: .local, isPaused: false)
|
||||||
|
view.setTestingService(service)
|
||||||
|
view.setTestingState(
|
||||||
|
mode: "serve",
|
||||||
|
requireCredentials: true,
|
||||||
|
password: "secret",
|
||||||
|
statusMessage: "Running")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func tailscaleSectionBuildsBodyForFunnelMode() {
|
||||||
|
let service = TailscaleService(
|
||||||
|
isInstalled: true,
|
||||||
|
isRunning: false,
|
||||||
|
tailscaleHostname: nil,
|
||||||
|
tailscaleIP: nil,
|
||||||
|
statusError: "not running")
|
||||||
|
var view = TailscaleIntegrationSection(connectionMode: .remote, isPaused: false)
|
||||||
|
view.setTestingService(service)
|
||||||
|
view.setTestingState(
|
||||||
|
mode: "funnel",
|
||||||
|
requireCredentials: false,
|
||||||
|
statusMessage: "Needs start",
|
||||||
|
validationMessage: "Invalid token")
|
||||||
|
_ = view.body
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user