ad6e80192d
Right-click menus: one portal shell (components/ContextMenu.tsx) for every item with actions - sidebar/Recents/archived session rows (SessionMenu), console tabs, pane headers, the Changes panel's files/tree/commits, the workspace home, sessions outside a workspace (SessionRowMenu: Sessions table, Dashboard, Activity, Workflow runs), prompt images, every Settings list and every Claude Code/Codex config list (FileItemMenu). Buttons stay; a delete keeps its button's confirm; the shell stops portal clicks from reaching the card that opened it. Status bar: Workspace $ and Session $ on the Workspace page, from ?lane= on GET /api/pricing/cost and the focused/replayed session's cost. Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB, hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created (replacing the redundant idx_events_session), watchdog reads sessions.transcript_path, transcript parsed before the hook's write transaction, GET /api/sessions/:id?events=0 for the web page (reloads only on its own session, debounced), streamed export, transactional clear-data and chunked cleanup, subagent import reads tool_use_ids once, runtime logs rotated past 5 MB.
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
/**
|
|
* @file StatusBar.test.tsx
|
|
* @description Tests for the shell's bottom status bar: the live/disconnected
|
|
* state with the version (click opens the connection details), the running and
|
|
* needs-you counts from the shared lane list, today's spend from the
|
|
* pricing cost call's `daily_costs`, and on the Workspace page the open
|
|
* workspace's and the shown session's totals.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi } from "vitest";
|
|
import { act, render, screen } from "@testing-library/react";
|
|
import { MemoryRouter } from "react-router-dom";
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
function today() {
|
|
const d = new Date();
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
}
|
|
|
|
vi.mock("../../lib/api", () => ({
|
|
api: {
|
|
lanes: {
|
|
list: vi.fn().mockResolvedValue({
|
|
lanes: [
|
|
{ id: 1, status: "running", needs_action: null },
|
|
{ id: 2, status: "running", needs_action: "permission" },
|
|
{ id: 3, status: "idle", needs_action: null },
|
|
],
|
|
counts: {},
|
|
}),
|
|
},
|
|
pricing: {
|
|
totalCost: vi.fn().mockImplementation(async (opts?: { lane?: number }) => ({
|
|
total_cost: opts?.lane ? 123.456 : 10,
|
|
breakdown: [],
|
|
daily_costs: [
|
|
{ date: "2000-01-01", cost: 9 },
|
|
{ date: today(), cost: 1.234 },
|
|
],
|
|
})),
|
|
sessionCost: vi.fn().mockImplementation(async (id: string) => ({
|
|
total_cost: id === "s-live" ? 4.5 : 7,
|
|
breakdown: [],
|
|
daily_costs: [],
|
|
})),
|
|
},
|
|
},
|
|
}));
|
|
|
|
import { api } from "../../lib/api";
|
|
import { StatusBar } from "../StatusBar";
|
|
import { LIVE_RUNS_EVENT } from "../WorkspaceTree";
|
|
|
|
const at = (url: string, ui: JSX.Element) =>
|
|
render(<MemoryRouter initialEntries={[url]}>{ui}</MemoryRouter>);
|
|
|
|
describe("StatusBar", () => {
|
|
it("shows the connection with the version, the counts and today's spend", async () => {
|
|
const user = userEvent.setup();
|
|
const onConnectionClick = vi.fn();
|
|
at("/", <StatusBar wsConnected left="15rem" onConnectionClick={onConnectionClick} />);
|
|
expect(screen.getByText("Live")).toBeInTheDocument();
|
|
expect(screen.getByText(`v${__APP_VERSION__}`)).toBeInTheDocument();
|
|
expect(await screen.findByText("Today $1.23")).toBeInTheDocument();
|
|
expect(screen.getByTestId("status-bar-counts").textContent).toBe("2 running · 1 need you");
|
|
await user.click(screen.getByText("Live"));
|
|
expect(onConnectionClick).toHaveBeenCalled();
|
|
});
|
|
|
|
it("says Disconnected when the socket is down", () => {
|
|
at("/", <StatusBar wsConnected={false} left="0" onConnectionClick={() => {}} />);
|
|
expect(screen.getByText("Disconnected")).toBeInTheDocument();
|
|
});
|
|
|
|
it("off the Workspace page shows no workspace or session cost", async () => {
|
|
at("/sessions", <StatusBar wsConnected left="0" onConnectionClick={() => {}} />);
|
|
await screen.findByText("Today $1.23");
|
|
expect(screen.queryByTestId("status-bar-workspace-cost")).toBeNull();
|
|
expect(screen.queryByTestId("status-bar-session-cost")).toBeNull();
|
|
});
|
|
|
|
it("shows the open workspace's total and the replayed session's", async () => {
|
|
at(
|
|
"/workspace?lane=3&session=s-old",
|
|
<StatusBar wsConnected left="0" onConnectionClick={() => {}} />
|
|
);
|
|
expect(await screen.findByText("Workspace $123.46")).toBeInTheDocument();
|
|
expect(await screen.findByText("Session $7.00")).toBeInTheDocument();
|
|
expect(api.pricing.totalCost).toHaveBeenCalledWith({ lane: 3 });
|
|
});
|
|
|
|
it("takes a focused pane's session from the page's run poll", async () => {
|
|
at(
|
|
"/workspace?lane=3&run=agenthub-lane-3-1",
|
|
<StatusBar wsConnected left="0" onConnectionClick={() => {}} />
|
|
);
|
|
await screen.findByText("Workspace $123.46");
|
|
expect(screen.queryByTestId("status-bar-session-cost")).toBeNull();
|
|
act(() => {
|
|
window.dispatchEvent(
|
|
new CustomEvent(LIVE_RUNS_EVENT, {
|
|
detail: [{ id: "agenthub-lane-3-1", laneId: 3, status: "running", sessionId: "s-live" }],
|
|
})
|
|
);
|
|
});
|
|
expect(await screen.findByText("Session $4.50")).toBeInTheDocument();
|
|
});
|
|
});
|