Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.16</string>
<string>0.1.17</string>
<key>CFBundleVersion</key>
<string>16</string>
<string>17</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
12 changes: 6 additions & 6 deletions Sources/CodexToolbar/Networking/CodexAppServerClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
20 changes: 18 additions & 2 deletions Tests/CodexToolbarTests/CodexAppServerClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,31 @@ final class CodexAppServerClientTests: XCTestCase {
XCTAssertEqual(status, .loggedOut)
}

func testParseLoginStatusTreatsPermissionErrorsAsLoggedOut() {
func testParseLoginStatusTreatsPermissionErrorsAsIndeterminate() {
let status = CodexAppServerClient.parseLoginStatus(
exitStatus: 1,
stdout: "",
stderr: "Error checking login status: Operation not permitted (os error 1)\n",
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() {
Expand Down
51 changes: 51 additions & 0 deletions Tests/CodexToolbarTests/RateLimitStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down