Skip to content

Commit 56a8cf4

Browse files
committed
Support fresh weekly reset activation flow
Add detection and activation flow for fresh weekly resets. Introduces shouldActivateFreshWeeklyReset(account:usage:now:) to determine eligibility (checks feature toggle, auth state, future weeklyResetAt, not already attempted, and >=99.5% remaining). Use it to show a "Fresh weekly reset detected. Activation pending." indicator, to trigger immediate runWeeklyAutoKickAttempt (with pause/retry handling), and to adjust scheduling logic. Adds unit tests to assert the pending indicator behavior and updates test helpers (makeAccount signature and a new makeUsage helper).
1 parent 4a4ba07 commit 56a8cf4

2 files changed

Lines changed: 104 additions & 3 deletions

File tree

CodexAccounts/ViewModels/AccountsViewModel.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,14 @@ final class AccountsViewModel {
980980
}
981981

982982
if let weeklyResetAt = usage.weeklyResetAt {
983+
if shouldActivateFreshWeeklyReset(account: current, usage: usage, now: now) {
984+
return WeeklyAutoKickIndicator(
985+
symbol: "bolt.circle.fill",
986+
color: .cyan,
987+
help: "Fresh weekly reset detected. Activation pending."
988+
)
989+
}
990+
983991
let secondsUntilReset = weeklyResetAt.timeIntervalSince(now)
984992
if secondsUntilReset <= weeklyAutoKickSoonThreshold {
985993
return WeeklyAutoKickIndicator(
@@ -1072,6 +1080,17 @@ final class AccountsViewModel {
10721080
return
10731081
}
10741082

1083+
if shouldActivateFreshWeeklyReset(account: account, usage: usage, now: now) {
1084+
let cycleID = usage.weeklyCycleIdentifier
1085+
if shouldPauseWeeklyAutoKick(account: account, cycleID: cycleID, now: now) {
1086+
scheduleRetryWeeklyAutoKickCheck(for: accountID, account: account, now: now)
1087+
return
1088+
}
1089+
1090+
await runWeeklyAutoKickAttempt(for: account, cycleID: cycleID, now: now)
1091+
return
1092+
}
1093+
10751094
if !usage.weeklyResetIsOverdue(now: now, grace: weeklyAutoKickDelay) {
10761095
scheduleNextWeeklyAutoKickCheck(for: accountID, usage: usage, now: now)
10771096
return
@@ -1124,6 +1143,28 @@ final class AccountsViewModel {
11241143
return false
11251144
}
11261145

1146+
private func shouldActivateFreshWeeklyReset(
1147+
account: CodexAccount,
1148+
usage: AccountUsage,
1149+
now: Date
1150+
) -> Bool {
1151+
guard isWeeklyAutoKickEnabled(for: account) else { return false }
1152+
guard account.authState != .needsReauth, account.authState != .degraded else { return false }
1153+
guard let weeklyResetAt = usage.weeklyResetAt,
1154+
let cycleID = usage.weeklyCycleIdentifier
1155+
else {
1156+
return false
1157+
}
1158+
guard weeklyResetAt > now else { return false }
1159+
guard account.lastWeeklyAutoKickCycleID != cycleID else { return false }
1160+
1161+
if let weeklyRemaining = usage.weeklyRemainingPercent {
1162+
return weeklyRemaining >= 99.5
1163+
}
1164+
1165+
return usage.isWeeklyPrimary && usage.remainingPercent >= 99.5
1166+
}
1167+
11271168
private func runWeeklyAutoKickAttempt(for account: CodexAccount, cycleID: String?, now: Date) async {
11281169
guard let attemptAccount = markWeeklyAutoKickAttempt(for: account.id, cycleID: cycleID, now: now) else { return }
11291170

@@ -1267,7 +1308,9 @@ final class AccountsViewModel {
12671308
let secondsUntilReset = weeklyResetAt.timeIntervalSince(now)
12681309
let nextCheckAt: Date
12691310

1270-
if secondsUntilReset <= -weeklyAutoKickDelay {
1311+
if shouldActivateFreshWeeklyReset(account: account, usage: usage, now: now) {
1312+
nextCheckAt = now
1313+
} else if secondsUntilReset <= -weeklyAutoKickDelay {
12711314
nextCheckAt = now.addingTimeInterval(weeklyAutoKickInterval)
12721315
} else if secondsUntilReset <= 0 {
12731316
nextCheckAt = weeklyResetAt.addingTimeInterval(weeklyAutoKickDelay)

CodexAccountsTests/CodexAccountsTests.swift

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,42 @@ struct CodexAccountsTests {
134134
#expect(!viewModel.isWeeklyAutoKickEnabled(for: forcedOff))
135135
}
136136

137+
@Test func freshWeeklyResetShowsActivationPendingForEligibleAccount() async throws {
138+
let viewModel = AccountsViewModel()
139+
let account = makeAccount(weeklyAutoKickOverride: .forceOn)
140+
let usage = makeUsage(
141+
weeklyUsedPercent: 0,
142+
weeklyResetAt: Date().addingTimeInterval(7 * 24 * 60 * 60 - 30 * 60)
143+
)
144+
145+
viewModel.accounts = [account]
146+
viewModel.usageData[account.id] = usage
147+
148+
let indicator = viewModel.weeklyAutoKickIndicator(for: account, usage: usage)
149+
150+
#expect(indicator?.help == "Fresh weekly reset detected. Activation pending.")
151+
}
152+
153+
@Test func freshWeeklyResetDoesNotShowPendingAfterCycleAttemptRecorded() async throws {
154+
let resetAt = Date().addingTimeInterval(7 * 24 * 60 * 60 - 30 * 60)
155+
let usage = makeUsage(
156+
weeklyUsedPercent: 0,
157+
weeklyResetAt: resetAt
158+
)
159+
let account = makeAccount(
160+
weeklyAutoKickOverride: .forceOn,
161+
lastWeeklyAutoKickCycleID: String(Int(resetAt.timeIntervalSince1970))
162+
)
163+
let viewModel = AccountsViewModel()
164+
165+
viewModel.accounts = [account]
166+
viewModel.usageData[account.id] = usage
167+
168+
let indicator = viewModel.weeklyAutoKickIndicator(for: account, usage: usage)
169+
170+
#expect(indicator?.help != "Fresh weekly reset detected. Activation pending.")
171+
}
172+
137173
@Test func pinnedAccountsRespectManualPinnedOrderBeforeSortedAccounts() async throws {
138174
let viewModel = AccountsViewModel()
139175
let firstPinned = makeAccount(
@@ -189,7 +225,8 @@ struct CodexAccountsTests {
189225
authState: AuthState = .healthy,
190226
isPinned: Bool = false,
191227
pinnedOrder: Int? = nil,
192-
weeklyAutoKickOverride: WeeklyAutoKickOverride = .inherit
228+
weeklyAutoKickOverride: WeeklyAutoKickOverride = .inherit,
229+
lastWeeklyAutoKickCycleID: String? = nil
193230
) -> CodexAccount {
194231
CodexAccount(
195232
email: email,
@@ -205,7 +242,28 @@ struct CodexAccountsTests {
205242
addedAt: Date(timeIntervalSince1970: 0),
206243
isPinned: isPinned,
207244
pinnedOrder: pinnedOrder,
208-
weeklyAutoKickOverride: weeklyAutoKickOverride
245+
weeklyAutoKickOverride: weeklyAutoKickOverride,
246+
lastWeeklyAutoKickCycleID: lastWeeklyAutoKickCycleID
247+
)
248+
}
249+
250+
private func makeUsage(
251+
weeklyUsedPercent: Double,
252+
weeklyResetAt: Date
253+
) -> AccountUsage {
254+
AccountUsage(
255+
usedPercent: weeklyUsedPercent,
256+
resetAt: weeklyResetAt,
257+
primaryWindowSeconds: 7 * 24 * 60 * 60,
258+
weeklyUsedPercent: weeklyUsedPercent,
259+
weeklyResetAt: weeklyResetAt,
260+
weeklyWindowSeconds: 7 * 24 * 60 * 60,
261+
creditsBalance: nil,
262+
hasCredits: false,
263+
isUnlimited: false,
264+
lastUpdated: Date(),
265+
error: nil,
266+
lastActivityAt: nil
209267
)
210268
}
211269

0 commit comments

Comments
 (0)