feat(workspace): the console tab bar is the session view's only header
The status/name/back-arrow row is gone; Tools sits at the tab bar's right end and the pipeline/feature pickers show only with a template. LaneConsolePane is reduced to the terminal (no title, no Active runs, no New run): RunHistory.tsx, api.run.history and the page's active-sessions poll are removed.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* @file LaneConsolePane.test.tsx
|
||||
* @description Test suite for the LaneConsolePane component: a pane shows its
|
||||
* lane's terminal or an empty state whose New session goes to the workspace
|
||||
* home — a pane has no run form of its own.
|
||||
* @description Test suite for the LaneConsolePane component: a tab's body shows
|
||||
* its lane's terminal or an empty state whose New session goes to the
|
||||
* workspace home — no run form and no header of its own.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -38,7 +38,6 @@ vi.mock("../../../lib/api", () => ({
|
||||
},
|
||||
run: {
|
||||
list: vi.fn().mockResolvedValue({ items: [] }),
|
||||
history: vi.fn().mockResolvedValue({ items: [] }),
|
||||
get: vi.fn(),
|
||||
start: vi.fn(),
|
||||
},
|
||||
@@ -85,16 +84,12 @@ function baseProps() {
|
||||
return {
|
||||
lanes: [LANE],
|
||||
laneId: 1,
|
||||
showLaneSelector: false,
|
||||
onLaneIdChange: vi.fn(),
|
||||
onLaneCreated: vi.fn(),
|
||||
binaryStatus: {
|
||||
claude: { found: true, path: "/usr/local/bin/claude" },
|
||||
codex: { found: true, path: "/usr/local/bin/codex" },
|
||||
antigravity: { found: true, path: "/usr/local/bin/agy" },
|
||||
},
|
||||
activeRuns: { items: [] },
|
||||
wsConnected: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,7 +108,7 @@ describe("LaneConsolePane", () => {
|
||||
expect(api.lanes.action).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("goes back to the home when a single pane's run exits, and on New run", async () => {
|
||||
it("has no header: no title, no Active runs, no New run - the tab bar is the header", async () => {
|
||||
const live = {
|
||||
id: "agenthub-lane-1",
|
||||
laneId: 1,
|
||||
@@ -128,22 +123,19 @@ describe("LaneConsolePane", () => {
|
||||
promptPreview: null,
|
||||
tool: "claude" as const,
|
||||
};
|
||||
const onNewSession = vi.fn();
|
||||
const onRunExit = vi.fn();
|
||||
render(
|
||||
<LaneConsolePane
|
||||
{...baseProps()}
|
||||
activeRuns={{ items: [live] }}
|
||||
onNewSession={onNewSession}
|
||||
onRunExit={onNewSession}
|
||||
/>
|
||||
<LaneConsolePane {...baseProps()} activeRuns={{ items: [live] }} onRunExit={onRunExit} />
|
||||
);
|
||||
await screen.findByTestId("terminal-view");
|
||||
fireEvent.click(screen.getByRole("button", { name: "New run" }));
|
||||
expect(onNewSession).toHaveBeenCalledTimes(1);
|
||||
expect(screen.queryByRole("heading")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "New run" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: /active run/i })).toBeNull();
|
||||
|
||||
// The run exits: the tab bar is told, and the pane is left empty.
|
||||
fireEvent.click(screen.getByTestId("fake-pane-exit"));
|
||||
await waitFor(() => expect(screen.queryByTestId("terminal-view")).not.toBeInTheDocument());
|
||||
expect(onNewSession).toHaveBeenCalledTimes(2);
|
||||
expect(onRunExit).toHaveBeenCalledOnce();
|
||||
expect(screen.getByTestId("pane-no-run")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -185,138 +177,12 @@ describe("LaneConsolePane", () => {
|
||||
await waitFor(() => expect(screen.queryByTestId("terminal-view")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("shows a lane dropdown only when showLaneSelector is true", () => {
|
||||
const { rerender } = render(<LaneConsolePane {...baseProps()} showLaneSelector />);
|
||||
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
|
||||
|
||||
rerender(<LaneConsolePane {...baseProps()} showLaneSelector={false} />);
|
||||
expect(screen.queryByTestId("pane-lane-select")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the console body when laneId is null and showLaneSelector is false (layout-1, fresh install)", () => {
|
||||
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={false} />);
|
||||
it("renders the console body when laneId is null (fresh install)", () => {
|
||||
render(<LaneConsolePane {...baseProps()} laneId={null} />);
|
||||
expect(screen.getByTestId("console-body")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("pane-empty")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("attaches to the lane's still-live session instead of failing when Resume hits ERUNLIVE", async () => {
|
||||
const HISTORY_ITEM = {
|
||||
id: "run-old",
|
||||
session_id: "sess-old",
|
||||
cwd: "/workspace/a",
|
||||
model: null,
|
||||
permission_mode: null,
|
||||
effort: null,
|
||||
resume_session_id: null,
|
||||
prompt_preview: "earlier prompt",
|
||||
status: "abandoned" as const,
|
||||
exit_code: null,
|
||||
started_at: new Date(1000).toISOString(),
|
||||
ended_at: new Date(2000).toISOString(),
|
||||
isLive: false,
|
||||
tool: "claude" as const,
|
||||
};
|
||||
(api.run.history as ReturnType<typeof vi.fn>).mockResolvedValue({ items: [HISTORY_ITEM] });
|
||||
(api.lanes.action as ReturnType<typeof vi.fn>).mockRejectedValue(
|
||||
// Matches the real shape `request()` throws: a plain, human-readable
|
||||
// `.message` plus a `.code` a caller actually branches on.
|
||||
Object.assign(new Error("lane already has a live run"), { code: "ERUNLIVE", status: 409 })
|
||||
);
|
||||
(api.lanes.list as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
lanes: [{ ...LANE, run_id: "run-live" }],
|
||||
});
|
||||
(api.run.get as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
id: "run-live",
|
||||
laneId: 1,
|
||||
status: "running",
|
||||
cwd: "/workspace/a",
|
||||
model: null,
|
||||
permissionMode: null,
|
||||
effort: null,
|
||||
resumeSessionId: null,
|
||||
sessionId: null,
|
||||
startedAt: null,
|
||||
promptPreview: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane {...baseProps()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: /active runs/i }));
|
||||
const resumeButtons = await screen.findAllByRole("button", {
|
||||
name: "Resume existing session",
|
||||
});
|
||||
fireEvent.click(resumeButtons[0]!);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId("terminal-view")).toHaveAttribute("data-run-id", "run-live")
|
||||
);
|
||||
expect(screen.getByText(/attached to it instead/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders an empty placeholder with selector when laneId is null but showLaneSelector is true (split-view)", () => {
|
||||
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={true} />);
|
||||
expect(screen.getByTestId("pane-empty")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("console-body")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("lists only this lane's runs and sessions in Active runs, not every lane's", () => {
|
||||
const run = (id: string, laneId: number, promptPreview: string) => ({
|
||||
id,
|
||||
laneId,
|
||||
status: "running",
|
||||
cwd: "/workspace/x",
|
||||
model: null,
|
||||
permissionMode: null,
|
||||
effort: null,
|
||||
resumeSessionId: null,
|
||||
sessionId: null,
|
||||
startedAt: "2026-09-24T01:00:00Z",
|
||||
promptPreview,
|
||||
});
|
||||
const session = (id: string, cwd: string, name: string) =>
|
||||
({
|
||||
id,
|
||||
cwd,
|
||||
name,
|
||||
model: null,
|
||||
source: "local",
|
||||
started_at: "2026-09-24T01:00:00Z",
|
||||
}) as never;
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane
|
||||
{...baseProps()}
|
||||
lanes={[
|
||||
LANE,
|
||||
{ ...LANE, id: 2, cwd: "/workspace/b" },
|
||||
// Nested inside lane 1: its sessions are ITS own, by longest prefix.
|
||||
{ ...LANE, id: 3, cwd: "/workspace/a/nested" },
|
||||
]}
|
||||
activeRuns={{ items: [run("r1", 1, "mine"), run("r2", 2, "other lane's")] as never }}
|
||||
externalSessions={[
|
||||
session("s1", "/workspace/a/src", "my session"),
|
||||
session("s2", "/workspace/b", "b session"),
|
||||
session("s3", "/workspace/a/nested/x", "nested session"),
|
||||
session("s4", "/workspace/ab", "sibling prefix"),
|
||||
]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /2 active runs/ }));
|
||||
expect(screen.getByText("mine")).toBeTruthy();
|
||||
expect(screen.getByText("my session")).toBeTruthy();
|
||||
expect(screen.queryByText("other lane's")).toBeNull();
|
||||
expect(screen.queryByText("b session")).toBeNull();
|
||||
expect(screen.queryByText("nested session")).toBeNull();
|
||||
expect(screen.queryByText("sibling prefix")).toBeNull();
|
||||
});
|
||||
|
||||
describe("split panes on one lane", () => {
|
||||
describe("tabs on one lane", () => {
|
||||
const run = (id: string, promptPreview = id) => ({
|
||||
id,
|
||||
laneId: 1,
|
||||
@@ -342,24 +208,6 @@ describe("LaneConsolePane", () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves runs other panes show out of Active runs", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<LaneConsolePane
|
||||
{...baseProps()}
|
||||
slot={1}
|
||||
activeRuns={{
|
||||
items: [run("agenthub-lane-1", "shown"), run("agenthub-lane-1-3", "free")],
|
||||
}}
|
||||
hiddenRunIds={["agenthub-lane-1"]}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: /1 active run/ }));
|
||||
expect(screen.getByText("free")).toBeTruthy();
|
||||
expect(screen.queryByText("shown")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("attaches to a run picked in the sidebar without starting or resuming anything", async () => {
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
/**
|
||||
* @file RunHistory.test.tsx
|
||||
* @description Pins the props-only boundary of `ActiveRunsSwitcher` / `RunsModal`
|
||||
* after their move out of `pages/Run.tsx`: the switcher counts live runs and
|
||||
* opens the list, the list merges live in-memory handles with persistent history
|
||||
* (live entries winning on a shared id) newest first, marks the live and current
|
||||
* rows, filters by status / mode / free text, and fires attach / resume / view
|
||||
* with the right run — attach by run id, resume and view with the matching
|
||||
* history item.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import { ActiveRunsSwitcher, RunsModal, type UnifiedRunRow } from "../RunHistory";
|
||||
import type { DashboardRunHistoryItem, RunListResponse } from "../../../lib/api";
|
||||
import type { Session } from "../../../lib/types";
|
||||
|
||||
const LIVE_ID = "run-live";
|
||||
const PAST_ID = "run-past";
|
||||
const HEADLESS_ID = "run-headless";
|
||||
|
||||
const activeRuns = {
|
||||
activeCount: 1,
|
||||
maxConcurrent: 2,
|
||||
items: [
|
||||
{
|
||||
id: LIVE_ID,
|
||||
sessionId: "sess-live",
|
||||
cwd: "/tmp/live",
|
||||
model: "claude-opus-5",
|
||||
status: "running",
|
||||
startedAt: "2000-01-01T00:50:00Z",
|
||||
promptPreview: "the live prompt",
|
||||
endedAt: null,
|
||||
},
|
||||
],
|
||||
} as unknown as RunListResponse;
|
||||
|
||||
function historyItem(over: Partial<DashboardRunHistoryItem>): DashboardRunHistoryItem {
|
||||
return {
|
||||
id: PAST_ID,
|
||||
session_id: "sess-past",
|
||||
cwd: "/tmp/past",
|
||||
model: "sonnet",
|
||||
status: "completed",
|
||||
prompt_preview: "the past prompt",
|
||||
started_at: new Date(2000).toISOString(),
|
||||
ended_at: new Date(2500).toISOString(),
|
||||
exit_code: 0,
|
||||
permission_mode: "acceptEdits",
|
||||
effort: "",
|
||||
resume_session_id: null,
|
||||
isLive: false,
|
||||
...over,
|
||||
} as DashboardRunHistoryItem;
|
||||
}
|
||||
|
||||
const PAST = historyItem({});
|
||||
const HEADLESS = historyItem({
|
||||
id: HEADLESS_ID,
|
||||
session_id: "sess-headless",
|
||||
cwd: "/tmp/headless",
|
||||
prompt_preview: "the headless prompt",
|
||||
started_at: new Date(1000).toISOString(),
|
||||
});
|
||||
|
||||
function externalSession(over: Partial<Session> = {}): Session {
|
||||
return {
|
||||
id: "sess-external",
|
||||
name: "the external prompt",
|
||||
status: "active",
|
||||
cwd: "/tmp/external",
|
||||
model: "claude-opus-5",
|
||||
started_at: new Date(3000).toISOString(),
|
||||
ended_at: null,
|
||||
updated_at: new Date(3000).toISOString(),
|
||||
source: "local",
|
||||
...over,
|
||||
} as unknown as Session;
|
||||
}
|
||||
|
||||
function renderSwitcher(overrides: Partial<React.ComponentProps<typeof ActiveRunsSwitcher>> = {}) {
|
||||
const spies = {
|
||||
onAttach: vi.fn(),
|
||||
onResumeFromHistory: vi.fn(),
|
||||
onViewFromHistory: vi.fn(),
|
||||
onKill: vi.fn(),
|
||||
onRefresh: vi.fn(),
|
||||
};
|
||||
const utils = render(
|
||||
<MemoryRouter>
|
||||
<ActiveRunsSwitcher
|
||||
activeRuns={activeRuns}
|
||||
currentHandleId={null}
|
||||
runHistory={[PAST, HEADLESS]}
|
||||
{...spies}
|
||||
{...overrides}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
return { ...utils, spies };
|
||||
}
|
||||
|
||||
function row(id: string, over: Partial<UnifiedRunRow> = {}): UnifiedRunRow {
|
||||
return {
|
||||
id,
|
||||
sessionId: `sess-${id}`,
|
||||
cwd: `/tmp/${id}`,
|
||||
model: "sonnet",
|
||||
status: "abandoned",
|
||||
promptPreview: `prompt of ${id}`,
|
||||
startedAt: 1000,
|
||||
endedAt: 2000,
|
||||
isLive: false,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function renderModal(
|
||||
rows: UnifiedRunRow[],
|
||||
overrides: Partial<React.ComponentProps<typeof RunsModal>> = {}
|
||||
) {
|
||||
const spies = {
|
||||
onAttach: vi.fn(),
|
||||
onResume: vi.fn(),
|
||||
onView: vi.fn(),
|
||||
onKill: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onRefresh: vi.fn(),
|
||||
};
|
||||
const utils = render(
|
||||
<MemoryRouter>
|
||||
<RunsModal
|
||||
rows={rows}
|
||||
currentHandleId={null}
|
||||
runHistory={[PAST, HEADLESS]}
|
||||
{...spies}
|
||||
{...overrides}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
return { ...utils, spies };
|
||||
}
|
||||
|
||||
/** The nth "All" chip — index 0 is the status group, 1 is the mode group. */
|
||||
function allChip(nth: number): HTMLElement {
|
||||
const hits = screen.getAllByText(i18n.t("run:runs.allLabel", "All"));
|
||||
const hit = hits[nth];
|
||||
if (!hit) throw new Error(`no "All" chip at index ${nth}`);
|
||||
return hit;
|
||||
}
|
||||
|
||||
/** A filter chip, told apart from the same word appearing in a row's status
|
||||
* pill or mode badge by being a `<button>`. */
|
||||
function chip(label: string): HTMLElement {
|
||||
const hit = screen.getAllByText(label).find((el) => el.tagName === "BUTTON");
|
||||
if (!hit) throw new Error(`no filter chip labelled ${label}`);
|
||||
return hit;
|
||||
}
|
||||
|
||||
const openModal = () =>
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
|
||||
|
||||
beforeEach(() => {
|
||||
i18n.changeLanguage("en");
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("ActiveRunsSwitcher", () => {
|
||||
it("labels the button with the live count and opens the list", () => {
|
||||
renderSwitcher();
|
||||
openModal();
|
||||
expect(screen.getByText("the live prompt")).toBeTruthy();
|
||||
expect(screen.getByText("the past prompt")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("falls back to the total count when nothing is live, and disables at zero", () => {
|
||||
const { unmount } = renderSwitcher({ activeRuns: null });
|
||||
expect(screen.getByText(i18n.t("run:runs.switcher"))).toBeTruthy();
|
||||
expect(screen.getByText("2")).toBeTruthy();
|
||||
unmount();
|
||||
|
||||
renderSwitcher({ activeRuns: null, runHistory: [] });
|
||||
const button = screen.getByText(i18n.t("run:runs.switcher")).closest("button");
|
||||
expect((button as HTMLButtonElement).disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("lists live runs first and marks the live one", () => {
|
||||
renderSwitcher();
|
||||
openModal();
|
||||
const prompts = screen
|
||||
.getAllByText(/the (live|past|headless) prompt/)
|
||||
.map((el) => el.textContent);
|
||||
expect(prompts).toEqual(["the live prompt", "the past prompt", "the headless prompt"]);
|
||||
expect(screen.getAllByText("live")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("prefers the live handle over a history row with the same id", () => {
|
||||
renderSwitcher({ runHistory: [historyItem({ id: LIVE_ID, prompt_preview: "stale copy" })] });
|
||||
openModal();
|
||||
expect(screen.getByText("the live prompt")).toBeTruthy();
|
||||
expect(screen.queryByText("stale copy")).toBeNull();
|
||||
});
|
||||
|
||||
it("counts and lists a session started outside the dashboard, resumable not attachable", () => {
|
||||
const { spies } = renderSwitcher({
|
||||
activeRuns: null,
|
||||
runHistory: [],
|
||||
externalSessions: [externalSession()],
|
||||
});
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
|
||||
expect(screen.getByText("the external prompt")).toBeTruthy();
|
||||
expect(screen.getByText(i18n.t("run:runs.externalBadge"))).toBeTruthy();
|
||||
// No tmux session of ours to attach to — Resume is the only action.
|
||||
expect(screen.queryByText(i18n.t("run:runs.attachLabel", "Attach"))).toBeNull();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
|
||||
expect(spies.onResumeFromHistory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "session:sess-external",
|
||||
session_id: "sess-external",
|
||||
cwd: "/tmp/external",
|
||||
status: "running",
|
||||
isLive: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("skips external sessions already covered by a run, remote ones, and cwd-less ones", () => {
|
||||
renderSwitcher({
|
||||
runHistory: [],
|
||||
externalSessions: [
|
||||
externalSession({ id: "sess-live" }), // same session as the live run
|
||||
externalSession({ id: "sess-remote", source: "remote-1" }),
|
||||
externalSession({ id: "sess-nocwd", cwd: null }),
|
||||
],
|
||||
});
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewActive_other", { count: 1 })));
|
||||
expect(screen.getByText("the live prompt")).toBeTruthy();
|
||||
expect(screen.queryByText("the external prompt")).toBeNull();
|
||||
expect(screen.queryByText(i18n.t("run:runs.externalBadge"))).toBeNull();
|
||||
});
|
||||
|
||||
it("shows a dash, not the epoch, for a row with no start time", () => {
|
||||
// A run whose persisted row is missing arrives with startedAt 0. Rendering
|
||||
// that as "Jan 1, 08:00 AM" presents a gap in the data as real data.
|
||||
renderModal([row("no-start", { startedAt: 0 })]);
|
||||
expect(screen.getByText("-")).toBeTruthy();
|
||||
expect(screen.queryByText(/Jan 1,/)).toBeNull();
|
||||
});
|
||||
|
||||
it("fires attach with the run id of the row that was clicked", () => {
|
||||
const { spies } = renderSwitcher();
|
||||
openModal();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.attachLabel", "Attach")));
|
||||
expect(spies.onAttach).toHaveBeenCalledWith(LIVE_ID);
|
||||
// Attaching closes the list, which is what the page relies on.
|
||||
expect(screen.queryByText("the past prompt")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("RunsModal", () => {
|
||||
it("offers Attach only for a live row that is not the current one", () => {
|
||||
const { unmount } = renderModal([row(LIVE_ID, { isLive: true, status: "running" })], {
|
||||
currentHandleId: LIVE_ID,
|
||||
});
|
||||
expect(screen.queryByText(i18n.t("run:runs.attachLabel", "Attach"))).toBeNull();
|
||||
expect(screen.getByText(i18n.t("run:runs.currentBadge", "current"))).toBeTruthy();
|
||||
unmount();
|
||||
|
||||
const { spies } = renderModal([row(LIVE_ID, { isLive: true, status: "running" })]);
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.attachLabel", "Attach")));
|
||||
expect(spies.onAttach).toHaveBeenCalledWith(LIVE_ID);
|
||||
});
|
||||
|
||||
it("needs two clicks to kill a live row, and never offers Kill for an external one", () => {
|
||||
const { spies, unmount } = renderModal([row(LIVE_ID, { isLive: true, status: "running" })]);
|
||||
const killButton = screen.getByText(i18n.t("run:runs.killLabel", "Kill"));
|
||||
fireEvent.click(killButton);
|
||||
expect(spies.onKill).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.killConfirmShort", "Sure?")));
|
||||
expect(spies.onKill).toHaveBeenCalledWith(LIVE_ID);
|
||||
unmount();
|
||||
|
||||
renderModal([row(LIVE_ID, { isLive: true, status: "running", external: true })]);
|
||||
expect(screen.queryByText(i18n.t("run:runs.killLabel", "Kill"))).toBeNull();
|
||||
});
|
||||
|
||||
it("fires resume with the history item behind a finished row", () => {
|
||||
const { spies } = renderModal([row(PAST_ID)]);
|
||||
fireEvent.click(screen.getByText(i18n.t("run:resume.resumeOption")));
|
||||
expect(spies.onResume).toHaveBeenCalledWith(PAST);
|
||||
});
|
||||
|
||||
it("fires view for a finished row", () => {
|
||||
const { spies } = renderModal([row(HEADLESS_ID)]);
|
||||
fireEvent.click(screen.getByText(i18n.t("run:runs.viewLabel")));
|
||||
expect(spies.onView).toHaveBeenCalledWith(HEADLESS);
|
||||
});
|
||||
|
||||
it("filters by status and by free text", () => {
|
||||
const rows = [
|
||||
row("a", { status: "running", isLive: true, promptPreview: "alpha" }),
|
||||
row("b", { status: "killed", promptPreview: "bravo" }),
|
||||
row("c", { status: "abandoned", promptPreview: "charlie" }),
|
||||
];
|
||||
renderModal(rows);
|
||||
|
||||
fireEvent.click(chip(i18n.t("run:status.killed")));
|
||||
expect(screen.getByText("bravo")).toBeTruthy();
|
||||
expect(screen.queryByText("alpha")).toBeNull();
|
||||
|
||||
fireEvent.click(allChip(0));
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText(
|
||||
i18n.t("run:runs.searchPlaceholder", "Search prompt, cwd, model, or session id…")
|
||||
),
|
||||
{ target: { value: "alpha" } }
|
||||
);
|
||||
expect(screen.getByText("alpha")).toBeTruthy();
|
||||
expect(screen.queryByText("charlie")).toBeNull();
|
||||
});
|
||||
|
||||
it("polls onRefresh while it is the foreground UI", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { spies } = renderModal([row("a")]);
|
||||
expect(spies.onRefresh).toHaveBeenCalledTimes(1);
|
||||
vi.advanceTimersByTime(4000);
|
||||
expect(spies.onRefresh).toHaveBeenCalledTimes(3);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user