Files
stanp-opencode/tests/tui.test.ts
T
StanPonomarev c48fe99b96
Release / release (push) Failing after 5s
chore: update opencode api to v2
2026-09-12 10:29:24 +02:00

105 lines
3.7 KiB
TypeScript

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();
}
});