relay-codex 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/docs/commands.md +6 -2
- package/docs/publishing.md +2 -2
- package/package.json +1 -1
- package/python/Menu.swift +52 -3
- package/python/commands.py +1 -0
- package/python/resets.py +30 -3
- package/python/router.py +1 -1
- package/tests/fake_native.py +14 -1
- package/tests/test_resets.py +63 -1
package/README.md
CHANGED
|
@@ -69,7 +69,8 @@ Use a session's menu to switch accounts after its work finishes. Account menus c
|
|
|
69
69
|
enable or disable future routing without stopping current tasks. Actions show
|
|
70
70
|
their result, and stopped workers offer a restart.
|
|
71
71
|
Account menus also show earned usage resets when available. Choose **Use available
|
|
72
|
-
reset…** and confirm to spend one; Relay refreshes allowance afterward.
|
|
72
|
+
reset…** and confirm to spend one; Relay refreshes allowance afterward. The button
|
|
73
|
+
is enabled only with weekly allowance at 0% and its reset more than 48 hours away. Recovery
|
|
73
74
|
never spends these resets automatically.
|
|
74
75
|
|
|
75
76
|
For tab completion in Zsh, run `source <(relay completion zsh)` after `compinit`.
|
package/docs/commands.md
CHANGED
|
@@ -51,13 +51,17 @@ A session cap of zero means unlimited. Removing an account retains its login
|
|
|
51
51
|
unless you request `--revoke`.
|
|
52
52
|
|
|
53
53
|
The account menu shows **Usage resets available** and **Use available reset…**
|
|
54
|
-
when Codex reports an earned reset.
|
|
54
|
+
when Codex reports an earned reset. It is enabled only when weekly allowance is
|
|
55
|
+
fully exhausted (0%) and the weekly reset is **more than 48 hours away**. Exactly
|
|
56
|
+
48 hours, any weekly allowance left, or unknown weekly data keeps it disabled.
|
|
57
|
+
The reason appears below the button. Choose it and confirm to use one. If the count
|
|
55
58
|
is unknown, choose **Check allowance now** first. Reset availability is separate
|
|
56
59
|
from purchased workspace credits. Resets are never spent by automatic recovery.
|
|
57
60
|
|
|
58
61
|
The equivalent terminal command is `relay account reset ACCOUNT --yes`. If a
|
|
59
62
|
response is lost, use the same action again: Relay saves and reuses the pending
|
|
60
|
-
request rather than submitting a new redemption.
|
|
63
|
+
request rather than submitting a new redemption. Pending retries must satisfy
|
|
64
|
+
the same weekly rule; their saved request is retained when locked. The menu calls this **Check
|
|
61
65
|
previous reset…**. An optional `--request-id UUID` also identifies a specific
|
|
62
66
|
logical attempt. Codex chooses the available reset; Relay checks the returned
|
|
63
67
|
result and reads allowance again afterward.
|
package/docs/publishing.md
CHANGED
|
@@ -8,8 +8,8 @@ Friends can run `npx relay-codex` or install it with `npm install -g relay-codex
|
|
|
8
8
|
The GitHub repository is enough for friends to install or try Relay:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npm install -g github:blackspider-ops/relay#v0.7.
|
|
12
|
-
npx --yes github:blackspider-ops/relay#v0.7.
|
|
11
|
+
npm install -g github:blackspider-ops/relay#v0.7.1
|
|
12
|
+
npx --yes github:blackspider-ops/relay#v0.7.1 --version
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
`npx` runs packages; it is not a separate publishing service. Publishing
|
package/package.json
CHANGED
package/python/Menu.swift
CHANGED
|
@@ -12,6 +12,8 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
12
12
|
var reconnect: DispatchWorkItem?
|
|
13
13
|
var stopping = false
|
|
14
14
|
var snapshotInFlight = false
|
|
15
|
+
var resetExpiry: DispatchWorkItem?
|
|
16
|
+
var menuOpen = false
|
|
15
17
|
let command = Bundle.main.object(forInfoDictionaryKey: "RelayCommand") as? String ?? "/usr/bin/false"
|
|
16
18
|
let state = Bundle.main.object(forInfoDictionaryKey: "RelayState") as? String ?? ""
|
|
17
19
|
|
|
@@ -140,6 +142,15 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
140
142
|
guard let seconds = value as? Double, seconds.isFinite, seconds > 0 else { return nil }
|
|
141
143
|
return seconds
|
|
142
144
|
}
|
|
145
|
+
static func resetBlockReason(_ account: [String: Any], now: Double = Date().timeIntervalSince1970) -> String? {
|
|
146
|
+
guard let weekly = account["secondary"] as? [String: Any], let used = (weekly["usedPercent"] as? NSNumber)?.doubleValue, used.isFinite else {
|
|
147
|
+
return "Reset locked: check allowance to confirm the weekly limit and reset time."
|
|
148
|
+
}
|
|
149
|
+
guard used >= 100 else { return "Reset locked: weekly allowance must be fully exhausted (0%)." }
|
|
150
|
+
guard let at = timestamp(weekly["resetsAt"]) else { return "Reset locked: check allowance to confirm the weekly limit and reset time." }
|
|
151
|
+
guard at - now > 48 * 60 * 60 else { return "Reset locked: the weekly reset must be more than 48 hours away." }
|
|
152
|
+
return nil
|
|
153
|
+
}
|
|
143
154
|
static func accountTimingRows(_ account: [String: Any]) -> [TimingRow] {
|
|
144
155
|
var rows: [TimingRow] = []
|
|
145
156
|
if let primary = account["primary"] as? [String: Any], let at = timestamp(primary["resetsAt"]) {
|
|
@@ -182,6 +193,7 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
182
193
|
updateTimes(menu)
|
|
183
194
|
if let visible = item.menu { MenuController.synchronise(visible, with: menu) }
|
|
184
195
|
else { item.menu = menu }
|
|
196
|
+
if menuOpen { scheduleResetExpiry() }
|
|
185
197
|
}
|
|
186
198
|
func makeMenu() -> NSMenu {
|
|
187
199
|
let sessions = current["sessions"] as? [[String: Any]] ?? []
|
|
@@ -233,8 +245,10 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
233
245
|
if let count = resetCount { text("Usage resets available: \(count)", into: submenu) }
|
|
234
246
|
else { text("Usage resets: check allowance to see availability.", into: submenu) }
|
|
235
247
|
if pendingReset || (resetCount ?? 0) > 0 {
|
|
248
|
+
let blocked = MenuController.resetBlockReason(account)
|
|
236
249
|
let reset = action(pendingReset ? "Check previous reset…" : "Use available reset…", args: ["confirm-reset", id, name, pendingReset ? "pending" : "new"], into: submenu)
|
|
237
|
-
reset.isEnabled = !actionRunning && !(account["maintenance"] as? Bool ?? false)
|
|
250
|
+
reset.isEnabled = !actionRunning && !(account["maintenance"] as? Bool ?? false) && blocked == nil
|
|
251
|
+
if let blocked = blocked { text(blocked, into: submenu) }
|
|
238
252
|
}
|
|
239
253
|
submenu.addItem(.separator())
|
|
240
254
|
let enabled = account["enabled"] as? Bool ?? true
|
|
@@ -298,12 +312,37 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
298
312
|
return menu
|
|
299
313
|
}
|
|
300
314
|
func menuWillOpen(_ menu: NSMenu) {
|
|
315
|
+
menuOpen = true
|
|
316
|
+
render()
|
|
301
317
|
refreshSnapshot()
|
|
302
318
|
updateTimes(menu)
|
|
303
319
|
for entry in menu.items where entry.title == "Open at login" {
|
|
304
320
|
entry.state = FileManager.default.fileExists(atPath: NSHomeDirectory() + "/Library/LaunchAgents/local.relay.menu.plist") ? .on : .off
|
|
305
321
|
}
|
|
306
322
|
}
|
|
323
|
+
func scheduleResetExpiry() {
|
|
324
|
+
resetExpiry?.cancel()
|
|
325
|
+
let now = Date().timeIntervalSince1970
|
|
326
|
+
let accounts = current["accounts"] as? [[String: Any]] ?? []
|
|
327
|
+
let boundaries = accounts.compactMap { account -> Double? in
|
|
328
|
+
guard MenuController.resetBlockReason(account, now: now) == nil, let weekly = account["secondary"] as? [String: Any], let at = MenuController.timestamp(weekly["resetsAt"]) else { return nil }
|
|
329
|
+
return at - 48 * 60 * 60
|
|
330
|
+
}
|
|
331
|
+
if let boundary = boundaries.min() {
|
|
332
|
+
let expiry = DispatchWorkItem { [weak self] in self?.render() }
|
|
333
|
+
resetExpiry = expiry
|
|
334
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + max(0, boundary - now), execute: expiry)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
func menuDidClose(_ menu: NSMenu) { menuOpen = false; resetExpiry?.cancel(); resetExpiry = nil }
|
|
338
|
+
func resetAllowed(_ id: String) -> Bool {
|
|
339
|
+
let accounts = current["accounts"] as? [[String: Any]] ?? []
|
|
340
|
+
guard let account = accounts.first(where: { $0["id"] as? String == id }) else {
|
|
341
|
+
actionNotice = "Reset locked: account details are unavailable."; actionFailed = true; render(); return false
|
|
342
|
+
}
|
|
343
|
+
if let reason = MenuController.resetBlockReason(account) { actionNotice = reason; actionFailed = true; render(); return false }
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
307
346
|
@objc func runAction(_ sender: NSMenuItem) {
|
|
308
347
|
guard var args = sender.representedObject as? [String], !args.isEmpty else { return }
|
|
309
348
|
if args[0] == "copy" { NSPasteboard.general.clearContents(); NSPasteboard.general.setString(args[1], forType: .string); return }
|
|
@@ -317,12 +356,14 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
317
356
|
args = ["retry", args[1], "--wait"]
|
|
318
357
|
}
|
|
319
358
|
if args[0] == "confirm-reset" {
|
|
359
|
+
guard resetAllowed(args[1]) else { return }
|
|
320
360
|
let pending = args[3] == "pending"
|
|
321
361
|
let alert = NSAlert(); alert.messageText = pending ? "Check the previous reset?" : "Use one usage reset?"
|
|
322
362
|
alert.informativeText = args[2] + "\n\n" + (pending ? "Relay will check the same pending request. If it already used a reset, another one will not be spent." : "This spends one earned reset on eligible usage limits for this account. Allowance will be checked afterward. Paused tasks and review holds stay paused.")
|
|
323
363
|
alert.addButton(withTitle: pending ? "Check reset" : "Use reset"); alert.addButton(withTitle: "Cancel")
|
|
324
364
|
NSApp.activate(ignoringOtherApps: true)
|
|
325
365
|
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
|
366
|
+
guard resetAllowed(args[1]) else { return }
|
|
326
367
|
args = ["account", "reset", args[1], "--yes", "--request-id", UUID().uuidString]
|
|
327
368
|
}
|
|
328
369
|
actionRunning = true; actionFailed = false; actionNotice = "Applying change…"; render()
|
|
@@ -413,11 +454,18 @@ if CommandLine.arguments.contains("--self-check") {
|
|
|
413
454
|
let correctEmpty = empty.items.contains { $0.title.contains("No Relay sessions running") } && empty.items.first { $0.identifier?.rawValue == "account:one" }?.state == .off
|
|
414
455
|
controller.current["sessions"] = originalSessions
|
|
415
456
|
let originalAccounts = controller.current["accounts"]
|
|
416
|
-
controller.current["accounts"] = [["id": "one", "name": "One", "reset_credits": 2], ["id": "two", "name": "Two", "reset_credits": 0], ["id": "unknown", "name": "Unknown"], ["id": "pending", "name": "Pending", "reset_pending": true]]
|
|
457
|
+
controller.current["accounts"] = [["id": "one", "name": "One", "reset_credits": 2, "secondary": ["usedPercent": 100, "resetsAt": Date().timeIntervalSince1970 + 72 * 3600]], ["id": "two", "name": "Two", "reset_credits": 0], ["id": "unknown", "name": "Unknown"], ["id": "pending", "name": "Pending", "reset_pending": true]]
|
|
417
458
|
let resets = controller.makeMenu()
|
|
418
459
|
let correctResets = findAction(["confirm-reset", "one", "One", "new"], in: resets)?.isEnabled == true && findAction(["confirm-reset", "two", "Two", "new"], in: resets) == nil && findAction(["confirm-reset", "unknown", "Unknown", "new"], in: resets) == nil && findAction(["confirm-reset", "pending", "Pending", "pending"], in: resets) != nil
|
|
419
460
|
controller.actionRunning = true
|
|
420
461
|
let resetLocked = findAction(["confirm-reset", "one", "One", "new"], in: controller.makeMenu())?.isEnabled == false
|
|
462
|
+
let policyNow = 2000000000.0
|
|
463
|
+
let far: [String: Any] = ["secondary": ["usedPercent": 100, "resetsAt": policyNow + 172801]]
|
|
464
|
+
let exact: [String: Any] = ["secondary": ["usedPercent": 100, "resetsAt": policyNow + 172800]]
|
|
465
|
+
let near: [String: Any] = ["secondary": ["usedPercent": 100, "resetsAt": policyNow + 172799]]
|
|
466
|
+
let left: [String: Any] = ["secondary": ["usedPercent": 99.99, "resetsAt": policyNow + 259200]]
|
|
467
|
+
let correctPolicy = MenuController.resetBlockReason(far, now: policyNow) == nil && MenuController.resetBlockReason(exact, now: policyNow) != nil && MenuController.resetBlockReason(near, now: policyNow) != nil && MenuController.resetBlockReason(left, now: policyNow) != nil && MenuController.resetBlockReason([:], now: policyNow) != nil
|
|
468
|
+
let pendingLocked = findAction(["confirm-reset", "pending", "Pending", "pending"], in: resets)?.isEnabled == false
|
|
421
469
|
controller.current["accounts"] = originalAccounts
|
|
422
470
|
controller.actionRunning = true; controller.actionNotice = "Applying change…"
|
|
423
471
|
let pending = controller.makeMenu()
|
|
@@ -429,7 +477,8 @@ if CommandLine.arguments.contains("--self-check") {
|
|
|
429
477
|
print("Pending action protected and result retained: \(correctPending && retainedResult)")
|
|
430
478
|
print("In-use accounts and current choice marked: \(correctMarker && correctMultiple && correctEmpty)")
|
|
431
479
|
print("Available and pending reset actions protected: \(correctResets && resetLocked)")
|
|
432
|
-
|
|
480
|
+
print("Weekly zero and more-than-48-hour guard enforced: \(correctPolicy && pendingLocked)")
|
|
481
|
+
exit(normal != nil && attention != nil && liveUpdate && safeIdentity && correctTiming && correctChoices && stoppedControls && correctPending && retainedResult && correctMarker && correctMultiple && correctEmpty && correctResets && resetLocked && correctPolicy && pendingLocked ? 0 : 1)
|
|
433
482
|
} else {
|
|
434
483
|
let app = NSApplication.shared
|
|
435
484
|
let delegate = MenuController(); app.delegate = delegate; app.run()
|
package/python/commands.py
CHANGED
|
@@ -114,6 +114,7 @@ def snapshot(state, include_inactive=False):
|
|
|
114
114
|
"usage_checked_at": pool.checked_at(label),
|
|
115
115
|
"reset_credits": resets.available(usage),
|
|
116
116
|
"reset_pending": resets.pending(state / "accounts" / label),
|
|
117
|
+
"reset_lock_reason": resets.weekly_block_reason(usage),
|
|
117
118
|
"maintenance": options.get("maintenance", False),
|
|
118
119
|
"next_check": min(
|
|
119
120
|
(b["next_check"] for b in blocks if b["account"] == label),
|
package/python/resets.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import json
|
|
5
|
+
import math
|
|
5
6
|
import time
|
|
6
7
|
import uuid
|
|
7
8
|
|
|
@@ -9,6 +10,29 @@ from pool import Pool, blocked, reset_time
|
|
|
9
10
|
from settings import atomic_json
|
|
10
11
|
|
|
11
12
|
OUTCOMES = {"reset", "alreadyRedeemed", "nothingToReset", "noCredit"}
|
|
13
|
+
WEEKLY_RESET_BUFFER = 48 * 60 * 60
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def weekly_block_reason(snapshot, now=None):
|
|
17
|
+
window = ((snapshot or {}).get("rateLimits") or {}).get("secondary")
|
|
18
|
+
if not isinstance(window, dict):
|
|
19
|
+
return (
|
|
20
|
+
"Reset locked: check allowance to confirm the weekly limit and reset time."
|
|
21
|
+
)
|
|
22
|
+
used, at = window.get("usedPercent"), window.get("resetsAt")
|
|
23
|
+
if type(used) not in (int, float) or not math.isfinite(used):
|
|
24
|
+
return (
|
|
25
|
+
"Reset locked: check allowance to confirm the weekly limit and reset time."
|
|
26
|
+
)
|
|
27
|
+
if used < 100:
|
|
28
|
+
return "Reset locked: weekly allowance must be fully exhausted (0%)."
|
|
29
|
+
if type(at) not in (int, float) or not math.isfinite(at):
|
|
30
|
+
return (
|
|
31
|
+
"Reset locked: check allowance to confirm the weekly limit and reset time."
|
|
32
|
+
)
|
|
33
|
+
if at - (time.time() if now is None else now) <= WEEKLY_RESET_BUFFER:
|
|
34
|
+
return "Reset locked: the weekly reset must be more than 48 hours away."
|
|
35
|
+
return None
|
|
12
36
|
|
|
13
37
|
|
|
14
38
|
def available(snapshot):
|
|
@@ -122,13 +146,16 @@ async def consume(state, label, binary, request_id=None):
|
|
|
122
146
|
)
|
|
123
147
|
if known and outcome == "reset":
|
|
124
148
|
outcome = "alreadyRedeemed"
|
|
125
|
-
if not
|
|
149
|
+
if not known:
|
|
126
150
|
current = await rpc.call("account/rateLimits/read")
|
|
127
151
|
update_allowance(pool, label, current)
|
|
152
|
+
reason = weekly_block_reason(current)
|
|
153
|
+
if reason:
|
|
154
|
+
raise RouterError(reason)
|
|
128
155
|
count = available(current)
|
|
129
|
-
if count == 0:
|
|
156
|
+
if count == 0 and not retry:
|
|
130
157
|
return {"outcome": "noCredit", "refreshed": True}
|
|
131
|
-
if count is None:
|
|
158
|
+
if count is None and not retry:
|
|
132
159
|
raise RouterError(
|
|
133
160
|
"Codex did not report reset availability. Check allowance again before using a reset."
|
|
134
161
|
)
|
package/python/router.py
CHANGED
|
@@ -24,7 +24,7 @@ from pool import Pool, blocked, reset_time, failure_kind, file_lock
|
|
|
24
24
|
from launch import translate, RESUME_FIELDS
|
|
25
25
|
import settings
|
|
26
26
|
|
|
27
|
-
VERSION = "0.7.
|
|
27
|
+
VERSION = "0.7.1"
|
|
28
28
|
_support = Path.home() / "Library/Application Support"
|
|
29
29
|
_legacy = _support / "Codex Accounts"
|
|
30
30
|
_default_state = _legacy if (_legacy / "accounts").exists() else _support / "Relay"
|
package/tests/fake_native.py
CHANGED
|
@@ -127,10 +127,21 @@ for raw in sys.stdin:
|
|
|
127
127
|
result = {"type": params["type"]}
|
|
128
128
|
elif method == "account/rateLimits/read":
|
|
129
129
|
reset_count = change(lambda d: d.get("reset_credit_count", 1))
|
|
130
|
+
weekly = change(
|
|
131
|
+
lambda d: (
|
|
132
|
+
{
|
|
133
|
+
"usedPercent": d["reset_weekly_used"],
|
|
134
|
+
"resetsAt": d["reset_weekly_at"],
|
|
135
|
+
}
|
|
136
|
+
if "reset_weekly_used" in d
|
|
137
|
+
else None
|
|
138
|
+
)
|
|
139
|
+
)
|
|
130
140
|
result = {
|
|
131
141
|
"ordinaryUsageAllowed": True,
|
|
132
142
|
"rateLimits": {
|
|
133
|
-
"primary": {"usedPercent": 20, "resetsAt": int(time.time()) + 3600}
|
|
143
|
+
"primary": {"usedPercent": 20, "resetsAt": int(time.time()) + 3600},
|
|
144
|
+
"secondary": weekly,
|
|
134
145
|
},
|
|
135
146
|
"rateLimitResetCredits": {
|
|
136
147
|
"availableCount": reset_count,
|
|
@@ -149,6 +160,8 @@ for raw in sys.stdin:
|
|
|
149
160
|
return {"outcome": "noCredit"}
|
|
150
161
|
keys.append(key)
|
|
151
162
|
data["reset_credit_count"] = count - 1
|
|
163
|
+
if "reset_weekly_used" in data:
|
|
164
|
+
data["reset_weekly_used"] = 0
|
|
152
165
|
return {"outcome": "reset"}
|
|
153
166
|
|
|
154
167
|
result = change(redeem)
|
package/tests/test_resets.py
CHANGED
|
@@ -27,6 +27,8 @@ class ResetService:
|
|
|
27
27
|
self.residual_limit = False
|
|
28
28
|
self.outcome = None
|
|
29
29
|
self.reject = False
|
|
30
|
+
self.weekly_used = 100
|
|
31
|
+
self.weekly_at = time.time() + 72 * 3600
|
|
30
32
|
|
|
31
33
|
async def call(self, method, params=None):
|
|
32
34
|
await asyncio.sleep(0)
|
|
@@ -39,7 +41,11 @@ class ResetService:
|
|
|
39
41
|
"primary": {
|
|
40
42
|
"usedPercent": 100 if self.blocked else 0,
|
|
41
43
|
"resetsAt": time.time() + 3600,
|
|
42
|
-
}
|
|
44
|
+
},
|
|
45
|
+
"secondary": {
|
|
46
|
+
"usedPercent": self.weekly_used,
|
|
47
|
+
"resetsAt": self.weekly_at,
|
|
48
|
+
},
|
|
43
49
|
},
|
|
44
50
|
"rateLimitResetCredits": {
|
|
45
51
|
"availableCount": self.count,
|
|
@@ -194,11 +200,67 @@ class ResetTests(unittest.IsolatedAsyncioTestCase):
|
|
|
194
200
|
with self.assertRaisesRegex(RouterError, "--yes"):
|
|
195
201
|
await commands.manage(args, self.root, "fake")
|
|
196
202
|
|
|
203
|
+
async def test_weekly_guard_blocks_all_mutation_until_zero_and_more_than_48_hours(
|
|
204
|
+
self,
|
|
205
|
+
):
|
|
206
|
+
for used, hours in ((0, 72), (99.99, 72), (100, 48), (100, 47), (100, -1)):
|
|
207
|
+
with self.subTest(used=used, hours=hours):
|
|
208
|
+
self.service.weekly_used = used
|
|
209
|
+
self.service.weekly_at = time.time() + hours * 3600
|
|
210
|
+
with self.assertRaisesRegex(RouterError, "Reset locked"):
|
|
211
|
+
await resets.consume(self.root, "one", "fake")
|
|
212
|
+
self.assertFalse(self.service.calls)
|
|
213
|
+
self.assertFalse((self.home / "reset-attempt.json").exists())
|
|
214
|
+
self.service.weekly_used = 100
|
|
215
|
+
self.service.weekly_at = time.time() + 72 * 3600
|
|
216
|
+
result = await resets.consume(self.root, "one", "fake")
|
|
217
|
+
self.assertEqual(result["outcome"], "reset")
|
|
218
|
+
|
|
219
|
+
async def test_pending_retry_cannot_bypass_a_changed_weekly_guard(self):
|
|
220
|
+
self.service.lose_reply = True
|
|
221
|
+
with self.assertRaises(RouterError):
|
|
222
|
+
await resets.consume(self.root, "one", "fake")
|
|
223
|
+
self.service.weekly_used = 0
|
|
224
|
+
with self.assertRaisesRegex(RouterError, "fully exhausted"):
|
|
225
|
+
await resets.consume(self.root, "one", "fake")
|
|
226
|
+
self.assertEqual(len(self.service.calls), 1)
|
|
227
|
+
self.assertTrue(resets.pending(self.home))
|
|
228
|
+
self.assertEqual(self.service.count, 1)
|
|
229
|
+
|
|
230
|
+
def test_weekly_guard_exact_boundary_unknown_values_and_unrounded_allowance(self):
|
|
231
|
+
now = 2000000000
|
|
232
|
+
|
|
233
|
+
def usage(used, at):
|
|
234
|
+
return {"rateLimits": {"secondary": {"usedPercent": used, "resetsAt": at}}}
|
|
235
|
+
|
|
236
|
+
self.assertIsNone(resets.weekly_block_reason(usage(100, now + 172801), now))
|
|
237
|
+
for used, at in (
|
|
238
|
+
(100, now + 172800),
|
|
239
|
+
(100, now + 172799),
|
|
240
|
+
(99.999, now + 200000),
|
|
241
|
+
(100, None),
|
|
242
|
+
(None, now + 200000),
|
|
243
|
+
(float("nan"), now + 200000),
|
|
244
|
+
(100, float("inf")),
|
|
245
|
+
):
|
|
246
|
+
self.assertIsNotNone(resets.weekly_block_reason(usage(used, at), now))
|
|
247
|
+
self.assertIsNotNone(resets.weekly_block_reason({}, now))
|
|
248
|
+
|
|
197
249
|
async def test_real_jsonl_helper_redeems_and_refreshes_synthetic_account(self):
|
|
198
250
|
from test_services import fake_executable
|
|
199
251
|
|
|
200
252
|
self.patch.stop()
|
|
201
253
|
binary = fake_executable(self.root)
|
|
254
|
+
atomic_json(
|
|
255
|
+
self.home / "fake-history.json",
|
|
256
|
+
{
|
|
257
|
+
"threads": {},
|
|
258
|
+
"calls": [],
|
|
259
|
+
"reset_credit_count": 1,
|
|
260
|
+
"reset_weekly_used": 100,
|
|
261
|
+
"reset_weekly_at": time.time() + 72 * 3600,
|
|
262
|
+
},
|
|
263
|
+
)
|
|
202
264
|
result = await resets.consume(self.root, "one", binary)
|
|
203
265
|
self.assertEqual(result, {"outcome": "reset", "refreshed": True})
|
|
204
266
|
history = json.loads((self.home / "fake-history.json").read_text())
|