add quota to the main screen
CI / verify (pull_request) Failing after 8s

This commit is contained in:
2026-09-17 22:06:45 +02:00
parent 9494936160
commit df61d9a282
4 changed files with 51 additions and 28 deletions
+1 -5
View File
@@ -237,11 +237,7 @@ export class QuotaTracker {
) {} ) {}
async init() { async init() {
try { await this.refresh();
await this.resolveCredential();
} catch {
return;
}
} }
private async resolveCredential(generation = this.generation) { private async resolveCredential(generation = this.generation) {
+10 -5
View File
@@ -60,9 +60,9 @@ export default Plugin.define({
setRevision((value) => value + 1); setRevision((value) => value + 1);
}); });
const Slot = (props: { sessionID: string }) => { const Slot = (props: { instance: string }) => {
const [status] = createResource( const [status] = createResource(
() => `${props.sessionID}:${revision()}`, () => `${props.instance}:${revision()}`,
() => loadQuotaStatus(() => quota.status({})), () => loadQuotaStatus(() => quota.status({})),
); );
return ( return (
@@ -83,12 +83,17 @@ export default Plugin.define({
); );
}; };
const remove = context.ui.slot({ const removeHome = context.ui.slot({
append: "home.footer.status",
render: () => <Slot instance="home" />,
});
const removeSidebar = context.ui.slot({
append: "sidebar.footer", append: "sidebar.footer",
render: (input) => <Slot sessionID={input.sessionID} />, render: (input) => <Slot instance={input.sessionID} />,
}); });
return () => { return () => {
remove(); removeHome();
removeSidebar();
unsubscribe(); unsubscribe();
clearInterval(retry); clearInterval(retry);
}; };
+9 -9
View File
@@ -329,11 +329,11 @@ test("reads old cached snapshots without session estimates and removes excluded
]); ]);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed"); expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
expect(result.stored.get("chatgpt-quota")).not.toHaveProperty("sessions"); expect(result.stored.get("chatgpt-quota")).not.toHaveProperty("sessions");
expect(result.calls()).toBe(0); expect(result.calls()).toBe(1);
await result.tracker.dispose(); 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]); const key = fixture({ type: "key", key: "secret" }, [plus]);
await key.tracker.init(); await key.tracker.init();
await key.tracker.start("root"); 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.init();
await oauth.tracker.start("root");
expect(oauth.calls()).toBe(1); 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"); expect(oauth.headers[0]?.get("chatgpt-account-id")).toBe("account-one");
await oauth.tracker.refresh();
const status = oauth.tracker.status(); const status = oauth.tracker.status();
expect(status?.windows[0]?.remaining).toBe(82.5); expect(status?.windows[0]?.remaining).toBe(82.5);
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed"); 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.init();
await result.tracker.start("root"); await result.tracker.start("root");
await result.tracker.complete("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"); await result.tracker.complete("root");
expect(result.calls()).toBe(2); expect(result.calls()).toBe(3);
expect(result.tracker.status()?.windows[0]?.remaining).toBe(89); expect(result.tracker.status()?.windows[0]?.remaining).toBe(70);
await result.tracker.dispose(); 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.init();
const old = result.tracker.start("root");
while (result.calls() === 0) await Bun.sleep(1); while (result.calls() === 0) await Bun.sleep(1);
result.switch({ result.switch({
type: "oauth", type: "oauth",
+31 -9
View File
@@ -5,7 +5,8 @@ import { createSignal } from "solid-js";
import { build } from "../scripts/build.js"; import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.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<string, never>) => JSX.Element;
const status = (plan: string, remaining: number): QuotaStatus => ({ const status = (plan: string, remaining: number): QuotaStatus => ({
plan, plan,
@@ -30,9 +31,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
); );
const { default: plugin } = tui; const { default: plugin } = tui;
const pending = Promise.withResolvers<QuotaStatus | null>(); const pending = Promise.withResolvers<QuotaStatus | null>();
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise]; const responses: Array<() => Promise<QuotaStatus | null>> = [
async () => status("home", 90),
() => pending.promise,
];
let statusCalls = 0; let statusCalls = 0;
let slotRender: SlotRender | undefined; let sidebarRender: SidebarSlotRender | undefined;
let homeRender: HomeSlotRender | undefined;
let notifyUpdated: (() => void) | undefined; let notifyUpdated: (() => void) | undefined;
const quota = { const quota = {
@@ -52,8 +57,17 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
const contextFixture = { const contextFixture = {
client: { rpc: () => quota }, client: { rpc: () => quota },
ui: { ui: {
slot(claim: { render: SlotRender }) { slot(claim: {
slotRender = claim.render; 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; 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. // members implemented by this focused rendering fixture.
const context = Object.assign({} as Plugin.Context, contextFixture); const context = Object.assign({} as Plugin.Context, contextFixture);
const cleanup = await plugin.setup(context); 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 [sessionID, setSessionID] = createSignal("session-one");
const renderSlot = slotRender; const renderSlot = sidebarRender;
const setup = await testRender( const setup = await testRender(
() => () =>
renderSlot({ renderSlot({
@@ -94,13 +116,13 @@ test("built quota slot stays reactive across pending, empty, failure, update, an
throw new Error("disconnected"); throw new Error("disconnected");
}); });
notifyUpdated(); notifyUpdated();
await setup.waitFor(() => statusCalls === 3); await setup.waitFor(() => statusCalls === 4);
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota"); expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
responses.push(async () => status("pro", 42)); responses.push(async () => status("pro", 42));
setSessionID("session-two"); setSessionID("session-two");
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)")); await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
expect(statusCalls).toBe(4); expect(statusCalls).toBe(5);
} finally { } finally {
setup.renderer.destroy(); setup.renderer.destroy();
if (cleanup) await cleanup(); if (cleanup) await cleanup();