This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Plugin } from "@opencode/plugin/tui";
|
||||
import { testRender, type JSX } from "@opentui/solid";
|
||||
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;
|
||||
|
||||
const status = (plan: string, remaining: number): QuotaStatus => ({
|
||||
plan,
|
||||
fetchedAt: 1,
|
||||
windows: [
|
||||
{
|
||||
id: "codex:primary",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 100 - remaining,
|
||||
resetsAt: 2,
|
||||
remaining,
|
||||
sessionConsumed: 1.5,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
test("built quota slot stays reactive across pending, empty, failure, update, and session states", async () => {
|
||||
await build();
|
||||
const { default: plugin } = await import("../dist/tui.js");
|
||||
const pending = Promise.withResolvers<QuotaStatus | null>();
|
||||
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise];
|
||||
const sessions: string[] = [];
|
||||
let slotRender: SlotRender | undefined;
|
||||
let notifyUpdated: (() => void) | undefined;
|
||||
|
||||
const quota = {
|
||||
status(input: { sessionID: string }) {
|
||||
sessions.push(input.sessionID);
|
||||
const response = responses.shift();
|
||||
if (!response) throw new Error("Missing test response");
|
||||
return response();
|
||||
},
|
||||
events: {
|
||||
on(_event: "updated", callback: () => void) {
|
||||
notifyUpdated = callback;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
const contextFixture = {
|
||||
client: { rpc: () => quota },
|
||||
ui: {
|
||||
slot(claim: { render: SlotRender }) {
|
||||
slotRender = claim.render;
|
||||
return () => undefined;
|
||||
},
|
||||
},
|
||||
theme: { text: { default: "#ffffff", muted: "#888888" } },
|
||||
};
|
||||
// SAFETY: The slot setup only reads the client RPC, slot registration, and theme
|
||||
// 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");
|
||||
|
||||
const [sessionID, setSessionID] = createSignal("session-one");
|
||||
const renderSlot = slotRender;
|
||||
const setup = await testRender(
|
||||
() =>
|
||||
renderSlot({
|
||||
get sessionID() {
|
||||
return sessionID();
|
||||
},
|
||||
}),
|
||||
{ width: 70, height: 5 },
|
||||
);
|
||||
|
||||
try {
|
||||
await setup.renderOnce();
|
||||
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
|
||||
|
||||
pending.resolve(status("plus", 75));
|
||||
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
|
||||
expect(setup.captureCharFrame()).toContain("5h: 75% left · ~1.5pp this session");
|
||||
|
||||
responses.push(async () => null);
|
||||
notifyUpdated();
|
||||
await setup.waitForFrame((frame) => !frame.includes("ChatGPT quota"));
|
||||
|
||||
responses.push(async () => {
|
||||
throw new Error("disconnected");
|
||||
});
|
||||
notifyUpdated();
|
||||
await setup.waitFor(() => sessions.length === 3);
|
||||
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(sessions).toEqual(["session-one", "session-one", "session-one", "session-two"]);
|
||||
} finally {
|
||||
setup.renderer.destroy();
|
||||
if (cleanup) await cleanup();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user