diff --git a/src/quota.ts b/src/quota.ts
index 3420249..cd20726 100644
--- a/src/quota.ts
+++ b/src/quota.ts
@@ -237,11 +237,7 @@ export class QuotaTracker {
) {}
async init() {
- try {
- await this.resolveCredential();
- } catch {
- return;
- }
+ await this.refresh();
}
private async resolveCredential(generation = this.generation) {
diff --git a/src/tui.tsx b/src/tui.tsx
index 479788b..d6e1b90 100644
--- a/src/tui.tsx
+++ b/src/tui.tsx
@@ -60,9 +60,9 @@ export default Plugin.define({
setRevision((value) => value + 1);
});
- const Slot = (props: { sessionID: string }) => {
+ const Slot = (props: { instance: string }) => {
const [status] = createResource(
- () => `${props.sessionID}:${revision()}`,
+ () => `${props.instance}:${revision()}`,
() => loadQuotaStatus(() => quota.status({})),
);
return (
@@ -83,12 +83,17 @@ export default Plugin.define({
);
};
- const remove = context.ui.slot({
+ const removeHome = context.ui.slot({
+ append: "home.footer.status",
+ render: () => ,
+ });
+ const removeSidebar = context.ui.slot({
append: "sidebar.footer",
- render: (input) => ,
+ render: (input) => ,
});
return () => {
- remove();
+ removeHome();
+ removeSidebar();
unsubscribe();
clearInterval(retry);
};
diff --git a/tests/quota.test.ts b/tests/quota.test.ts
index ae536d8..6bd1dab 100644
--- a/tests/quota.test.ts
+++ b/tests/quota.test.ts
@@ -329,11 +329,11 @@ test("reads old cached snapshots without session estimates and removes excluded
]);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
expect(result.stored.get("chatgpt-quota")).not.toHaveProperty("sessions");
- expect(result.calls()).toBe(0);
+ expect(result.calls()).toBe(1);
await result.tracker.dispose();
});
-test("activates only for OAuth and refreshes quota at request start", async () => {
+test("activates only for OAuth and refreshes quota at initialization and request start", async () => {
const key = fixture({ type: "key", key: "secret" }, [plus]);
await key.tracker.init();
await key.tracker.start("root");
@@ -360,10 +360,11 @@ test("activates only for OAuth and refreshes quota at request start", async () =
],
);
await oauth.tracker.init();
- await oauth.tracker.start("root");
expect(oauth.calls()).toBe(1);
+ expect(oauth.tracker.status()?.windows[0]?.remaining).toBe(87.5);
+ await oauth.tracker.start("root");
+ expect(oauth.calls()).toBe(2);
expect(oauth.headers[0]?.get("chatgpt-account-id")).toBe("account-one");
- await oauth.tracker.refresh();
const status = oauth.tracker.status();
expect(status?.windows[0]?.remaining).toBe(82.5);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
@@ -482,10 +483,10 @@ test("request completion refreshes once across terminal and idle events", async
await result.tracker.init();
await result.tracker.start("root");
await result.tracker.complete("root");
- expect(result.tracker.status()?.windows[0]?.remaining).toBe(89);
+ expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.complete("root");
- expect(result.calls()).toBe(2);
- expect(result.tracker.status()?.windows[0]?.remaining).toBe(89);
+ expect(result.calls()).toBe(3);
+ expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.dispose();
});
@@ -513,8 +514,7 @@ test("an auth switch detaches an in-flight old-account response and immediately
},
],
);
- await result.tracker.init();
- const old = result.tracker.start("root");
+ const old = result.tracker.init();
while (result.calls() === 0) await Bun.sleep(1);
result.switch({
type: "oauth",
diff --git a/tests/tui.test.ts b/tests/tui.test.ts
index 9d3a26f..323b62b 100644
--- a/tests/tui.test.ts
+++ b/tests/tui.test.ts
@@ -5,7 +5,8 @@ import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
-type SlotRender = (input: { readonly sessionID: string }) => JSX.Element;
+type SidebarSlotRender = (input: { readonly sessionID: string }) => JSX.Element;
+type HomeSlotRender = (input: Record) => JSX.Element;
const status = (plan: string, remaining: number): QuotaStatus => ({
plan,
@@ -30,9 +31,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
);
const { default: plugin } = tui;
const pending = Promise.withResolvers();
- const responses: Array<() => Promise> = [() => pending.promise];
+ const responses: Array<() => Promise> = [
+ async () => status("home", 90),
+ () => pending.promise,
+ ];
let statusCalls = 0;
- let slotRender: SlotRender | undefined;
+ let sidebarRender: SidebarSlotRender | undefined;
+ let homeRender: HomeSlotRender | undefined;
let notifyUpdated: (() => void) | undefined;
const quota = {
@@ -52,8 +57,17 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
const contextFixture = {
client: { rpc: () => quota },
ui: {
- slot(claim: { render: SlotRender }) {
- slotRender = claim.render;
+ slot(claim: {
+ append: "home.footer.status" | "sidebar.footer";
+ render: HomeSlotRender | SidebarSlotRender;
+ }) {
+ if (claim.append === "home.footer.status") {
+ // SAFETY: The home slot renderer receives the empty home slot input.
+ homeRender = claim.render as HomeSlotRender;
+ } else {
+ // SAFETY: The only other fixture path is the session-aware sidebar slot.
+ sidebarRender = claim.render as SidebarSlotRender;
+ }
return () => undefined;
},
},
@@ -63,10 +77,18 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
// members implemented by this focused rendering fixture.
const context = Object.assign({} as Plugin.Context, contextFixture);
const cleanup = await plugin.setup(context);
- if (!slotRender || !notifyUpdated) throw new Error("Plugin did not register its slot");
+ if (!sidebarRender || !homeRender || !notifyUpdated)
+ throw new Error("Plugin did not register its slots");
+
+ const renderHome = homeRender;
+ const home = await testRender(() => renderHome({}), { width: 70, height: 5 });
+ await home.waitForFrame((frame) => frame.includes("ChatGPT quota (home)"));
+ expect(home.captureCharFrame()).toContain("5h: 90% ยท due");
+ expect(statusCalls).toBe(1);
+ home.renderer.destroy();
const [sessionID, setSessionID] = createSignal("session-one");
- const renderSlot = slotRender;
+ const renderSlot = sidebarRender;
const setup = await testRender(
() =>
renderSlot({
@@ -94,13 +116,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
throw new Error("disconnected");
});
notifyUpdated();
- await setup.waitFor(() => statusCalls === 3);
+ await setup.waitFor(() => statusCalls === 4);
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
responses.push(async () => status("pro", 42));
setSessionID("session-two");
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
- expect(statusCalls).toBe(4);
+ expect(statusCalls).toBe(5);
} finally {
setup.renderer.destroy();
if (cleanup) await cleanup();