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
Binary file added .github/pr-assets/508-grok-coupon-unknown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 5 additions & 10 deletions gui/src/hooks/useGrokResetCoupons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type GrokCouponEntry =

export interface GrokRedeemOutcome {
ok: boolean;
/** Settled ledger code, or `aborted`/`network` when the request never settled. */
/** Settled ledger code, or `aborted` when delivery may have succeeded. */
code: string;
replayed: boolean;
}
Expand Down Expand Up @@ -101,11 +101,6 @@ function byExpiry(coupons: GrokResetCoupon[]): GrokResetCoupon[] {
return coupons.toSorted((a, b) => expiryRank(a) - expiryRank(b));
}

function wasAborted(error: unknown, signal: AbortSignal): boolean {
if (signal.aborted) return true;
return Boolean(error && typeof error === "object" && (error as { name?: unknown }).name === "AbortError");
}

export function useGrokResetCoupons({ apiBase, accountIds, enabled }: {
apiBase: string;
accountIds: string[];
Expand Down Expand Up @@ -196,10 +191,10 @@ export function useGrokResetCoupons({ apiBase, accountIds, enabled }: {
const code = settledCode(data);
await read(accountId, epoch.current);
return { ok: code === "redeemed", code, replayed };
} catch (error) {
// An aborted redemption is an unknown outcome, not a failure: the route may
// still be executing it. The caller must stop posting, not retry.
return { ok: false, code: wasAborted(error, bounded.signal) ? "aborted" : "network", replayed: false };
} catch {
// Any transport rejection after dispatch has an unknown outcome: the route
// may still be executing it. The caller must stop posting, not retry.
return { ok: false, code: "aborted", replayed: false };
} finally {
bounded.clear();
}
Expand Down
9 changes: 5 additions & 4 deletions gui/tests/grok-reset-coupons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* The Grok reset-coupon surface on xAI account rows.
*
* These cases exist because the dangerous paths here are the quiet ones: a
* replayed *failure* arrives as HTTP 200, an aborted redemption may still be
* executing upstream, and a per-row retry used to cancel every sibling read.
* replayed *failure* arrives as HTTP 200, a rejected redemption request may
* still be executing upstream, and a per-row retry used to cancel every sibling
* read.
*/
import { afterEach, beforeEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
Expand Down Expand Up @@ -262,9 +263,9 @@ test("ledger capacity gets its own retryable message", async () => {
expect(dialogText(host)).toContain("journal is full");
});

test("an aborted redemption stops posting, re-reads the account, and offers no retry", async () => {
test("a transport-rejected redemption stops posting, re-reads the account, and offers no retry", async () => {
harness.coupons.set("acct-a", [COUPON("restok_a1", 10)]);
harness.consumeReply = async () => { throw Object.assign(new Error("aborted"), { name: "AbortError" }); };
harness.consumeReply = async () => { throw new TypeError("connection reset after request dispatch"); };
const host = await mountPanel([ACCOUNT("acct-a")]);

await openDialog(host);
Expand Down
Loading