diff --git a/Resources/Info.plist b/Resources/Info.plist
index 9ece575..4aa1508 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -19,9 +19,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.1.16
+ 0.1.17
CFBundleVersion
- 16
+ 17
LSMinimumSystemVersion
14.0
LSUIElement
diff --git a/Sources/CodexToolbar/Networking/CodexAppServerClient.swift b/Sources/CodexToolbar/Networking/CodexAppServerClient.swift
index d090971..2276de8 100644
--- a/Sources/CodexToolbar/Networking/CodexAppServerClient.swift
+++ b/Sources/CodexToolbar/Networking/CodexAppServerClient.swift
@@ -461,12 +461,12 @@ actor CodexAppServerClient: CodexRateLimitClient {
.filter { !$0.isEmpty }
.joined(separator: "\n")
- let lowercased = combined.lowercased()
- if lowercased.contains("not logged in")
- || lowercased.contains("operation not permitted")
- || lowercased.contains("permission denied")
- || lowercased.contains("os error 1")
- {
+ // Only an explicit login-status result establishes that the user is
+ // signed out. Access failures cannot tell us whether credentials exist.
+ let reportsLoggedOut = combined.split(whereSeparator: \.isNewline).contains {
+ $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() == "not logged in"
+ }
+ if reportsLoggedOut {
return .loggedOut
}
diff --git a/Tests/CodexToolbarTests/CodexAppServerClientTests.swift b/Tests/CodexToolbarTests/CodexAppServerClientTests.swift
index 19028b9..bd20a3b 100644
--- a/Tests/CodexToolbarTests/CodexAppServerClientTests.swift
+++ b/Tests/CodexToolbarTests/CodexAppServerClientTests.swift
@@ -88,7 +88,7 @@ final class CodexAppServerClientTests: XCTestCase {
XCTAssertEqual(status, .loggedOut)
}
- func testParseLoginStatusTreatsPermissionErrorsAsLoggedOut() {
+ func testParseLoginStatusTreatsPermissionErrorsAsIndeterminate() {
let status = CodexAppServerClient.parseLoginStatus(
exitStatus: 1,
stdout: "",
@@ -96,7 +96,23 @@ final class CodexAppServerClientTests: XCTestCase {
timedOut: false
)
- XCTAssertEqual(status, .loggedOut)
+ XCTAssertEqual(status, .indeterminate("Error checking login status: Operation not permitted (os error 1)"))
+ }
+
+ func testParseLoginStatusDoesNotInferLogoutFromDiagnosticText() {
+ for diagnostic in ["Permission denied", "os error 1", "Unable to determine whether user is not logged in"] {
+ XCTAssertEqual(
+ CodexAppServerClient.parseLoginStatus(exitStatus: 1, stdout: "", stderr: diagnostic, timedOut: false),
+ .indeterminate(diagnostic)
+ )
+ }
+ }
+
+ func testParseLoginStatusTimeoutDoesNotEstablishLogout() {
+ XCTAssertEqual(
+ CodexAppServerClient.parseLoginStatus(exitStatus: 1, stdout: "Not logged in", stderr: "", timedOut: true),
+ .indeterminate("Timed out checking Codex login status.")
+ )
}
func testParseLoginStatusTreatsUnknownFailuresAsIndeterminate() {
diff --git a/Tests/CodexToolbarTests/RateLimitStoreTests.swift b/Tests/CodexToolbarTests/RateLimitStoreTests.swift
index cecf904..3ff9e13 100644
--- a/Tests/CodexToolbarTests/RateLimitStoreTests.swift
+++ b/Tests/CodexToolbarTests/RateLimitStoreTests.swift
@@ -909,6 +909,57 @@ final class RateLimitStoreTests: XCTestCase {
XCTAssertEqual(client.loadSnapshotCallCount, 2)
}
+ func testManualRefreshPermissionFailureDoesNotReportSignInOrDiscardCards() async {
+ let client = FakeCodexRateLimitClient()
+ let permissionStatus = CodexAppServerClient.parseLoginStatus(
+ exitStatus: 1, stdout: "",
+ stderr: "Error checking login status: Permission denied (os error 13)",
+ timedOut: false
+ )
+ client.loginStatusResults = [.success(.loggedIn), .success(permissionStatus)]
+ let store = RateLimitStore(client: client, reconnectDelayNanoseconds: 10_000_000_000,
+ refreshDelayNanosecondsProvider: { 10_000_000_000 })
+ await store.start()
+ let initialRemaining = store.cards.map(\.remainingPercent)
+ await store.refreshNow()
+
+ XCTAssertEqual(client.loadSnapshotCallCount, 2)
+ XCTAssertEqual(store.state, .ready)
+ XCTAssertEqual(store.cards.map(\.remainingPercent), initialRemaining)
+ XCTAssertNil(store.staleMessage)
+ await store.stop()
+ }
+
+ func testTimeoutWithPermissionFailurePreservesStaleDataAndRecovers() async {
+ let client = FakeCodexRateLimitClient()
+ let permissionStatus = CodexAppServerClient.parseLoginStatus(
+ exitStatus: 1, stdout: "",
+ stderr: "Error checking login status: Operation not permitted (os error 1)",
+ timedOut: false
+ )
+ client.loginStatusResults = [.success(.loggedIn), .success(.loggedIn), .success(permissionStatus)]
+ let store = RateLimitStore(client: client, reconnectDelayNanoseconds: 10_000_000_000,
+ refreshDelayNanosecondsProvider: { 10_000_000_000 })
+ await store.start()
+ let initialCards = store.cards
+ let initialLastUpdated = store.lastUpdated
+ let timeoutMessage = "Timed out reading Codex rate limits."
+ client.loadSnapshotError = CodexAppServerError.serverError(timeoutMessage)
+ await store.refreshNow()
+
+ XCTAssertEqual(store.state, .error(timeoutMessage))
+ XCTAssertEqual(store.statusBarAccessibilityText, timeoutMessage)
+ XCTAssertEqual(store.staleMessage, timeoutMessage)
+ XCTAssertEqual(store.cards, initialCards)
+ XCTAssertEqual(store.lastUpdated, initialLastUpdated)
+
+ client.loadSnapshotError = nil
+ await store.refreshNow()
+ XCTAssertEqual(store.state, .ready)
+ XCTAssertNil(store.staleMessage)
+ await store.stop()
+ }
+
func testStartupIgnoresRequiresOpenaiAuthWhenAccountIsPresent() async {
let client = FakeCodexRateLimitClient()
client.accountResponse = GetAccountResponse(