feat(run): Enter sends the prompt, and a busy lane attaches instead of failing
- The prompt box sends on Enter (Ctrl/Cmd+Enter still works); Shift+Enter inserts a newline, and IME composition (Vietnamese/CJK) never sends. - Starting or resuming into a lane that still holds a live run answered a raw 409 ERUNLIVE; the console now attaches to that run with a notice. Checked by error code, since the server's message is prose. - Active runs gains a two-click Kill for live dashboard-spawned rows, the way to free a lane when the previous session really should go.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { LaneConsolePane } from "../LaneConsolePane";
|
||||
import { api } from "../../../lib/api";
|
||||
import type { Lane } from "../../../lib/types";
|
||||
@@ -266,6 +267,64 @@ describe("LaneConsolePane", () => {
|
||||
await waitFor(() => expect(prompt.value).toBe("add a button"));
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user