Files
openclaw/apps/macos/Tests/OpenClawIPCTests/CritterStatusBeatsTests.swift
Peter Steinberger 94410b00d0 fix(macos): show gateway auth failures in app status (#118841)
* fix(macos): surface live gateway connection state

* chore(i18n): defer generated locale refresh

* fix(macos): preserve sleeping gateway badge state
2026-08-03 11:52:24 -07:00

130 lines
4.7 KiB
Swift

import Foundation
import Testing
@testable import OpenClaw
@MainActor
struct CritterStatusBeatsTests {
@Test func `failed gateway recovery triggers reconnect beat`() {
#expect(CritterStatusLabel.reconnectBeat(
lastSettled: .failed("boom"),
to: .running(details: nil)))
}
@Test func `recovery through starting still reads as a comeback`() {
// failed -> starting -> running: .starting never settles, so the
// running transition is still judged against the failure.
#expect(!CritterStatusLabel.isSettled(.starting))
#expect(CritterStatusLabel.reconnectBeat(
lastSettled: .failed("boom"),
to: .attachedExisting(details: nil)))
}
@Test func `cold start stays quiet`() {
#expect(!CritterStatusLabel.reconnectBeat(lastSettled: nil, to: .running(details: nil)))
}
@Test func `deliberate stop then start stays quiet`() {
#expect(!CritterStatusLabel.reconnectBeat(lastSettled: .stopped, to: .running(details: nil)))
}
@Test func `already running gateway stays quiet`() {
#expect(!CritterStatusLabel.reconnectBeat(
lastSettled: .running(details: nil),
to: .running(details: nil)))
}
@Test func `failure after failure stays quiet`() {
#expect(!CritterStatusLabel.reconnectBeat(lastSettled: .failed("boom"), to: .failed("again")))
#expect(!CritterStatusLabel.reconnectBeat(lastSettled: .failed("boom"), to: .stopped))
#expect(!CritterStatusLabel.reconnectBeat(lastSettled: .failed("boom"), to: .starting))
}
@Test func `every non-starting status settles`() {
#expect(CritterStatusLabel.isSettled(.stopped))
#expect(CritterStatusLabel.isSettled(.running(details: nil)))
#expect(CritterStatusLabel.isSettled(.attachedExisting(details: "pid 1")))
#expect(CritterStatusLabel.isSettled(.failed("boom")))
}
@Test func `missing work start stays quiet`() {
let endedAt = Date(timeIntervalSinceReferenceDate: 100)
#expect(!CritterStatusLabel.workCompletionBeat(
startedAt: nil,
endedAt: endedAt,
minimumDuration: 10))
}
@Test func `short work stays quiet`() {
let startedAt = Date(timeIntervalSinceReferenceDate: 100)
#expect(!CritterStatusLabel.workCompletionBeat(
startedAt: startedAt,
endedAt: startedAt.addingTimeInterval(5),
minimumDuration: 10))
}
@Test func `long work triggers completion beat`() {
let startedAt = Date(timeIntervalSinceReferenceDate: 100)
#expect(CritterStatusLabel.workCompletionBeat(
startedAt: startedAt,
endedAt: startedAt.addingTimeInterval(15),
minimumDuration: 10))
}
@Test func `minimum work duration triggers completion beat`() {
let startedAt = Date(timeIntervalSinceReferenceDate: 100)
#expect(CritterStatusLabel.workCompletionBeat(
startedAt: startedAt,
endedAt: startedAt.addingTimeInterval(10),
minimumDuration: 10))
}
@Test func `remote authentication failure raises attention and recovery clears it`() {
let gatewayStatus = GatewayProcessManager.Status.stopped
let authFailure = RemoteGatewayAuthIssue.tokenMismatch.statusMessage
#expect(CritterStatusLabel.needsAttention(
connectionMode: .remote,
controlState: .degraded(authFailure),
gatewayStatus: gatewayStatus,
isPaused: false,
isSleeping: true))
#expect(!CritterStatusLabel.needsAttention(
connectionMode: .remote,
controlState: .connected,
gatewayStatus: gatewayStatus,
isPaused: false,
isSleeping: false))
}
@Test func `attention follows the configured owner`() {
#expect(CritterStatusLabel.needsAttention(
connectionMode: .local,
controlState: .connected,
gatewayStatus: .failed("boom"),
isPaused: false,
isSleeping: false))
#expect(!CritterStatusLabel.needsAttention(
connectionMode: .local,
controlState: .connected,
gatewayStatus: .stopped,
isPaused: false,
isSleeping: true))
#expect(!CritterStatusLabel.needsAttention(
connectionMode: .unconfigured,
controlState: .degraded("ignored"),
gatewayStatus: .stopped,
isPaused: false,
isSleeping: false))
#expect(!CritterStatusLabel.needsAttention(
connectionMode: .remote,
controlState: .degraded("auth failed"),
gatewayStatus: .stopped,
isPaused: true,
isSleeping: true))
}
}