story-kit: merge 155_story_queue_messages_while_agent_is_busy
This commit is contained in:
@@ -555,3 +555,243 @@ describe("Chat activity status indicator (Bug 140)", () => {
|
||||
expect(indicator).toHaveTextContent("Using SomeCustomTool...");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Chat message queue (Story 155)", () => {
|
||||
beforeEach(() => {
|
||||
capturedWsHandlers = null;
|
||||
setupMocks();
|
||||
});
|
||||
|
||||
it("shows queued message indicator when submitting while loading (AC1, AC2)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
// Send first message to put the chat in loading state
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First message" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Now type and submit a second message while loading is true
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Queued message" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// The queued message indicator should appear
|
||||
const indicator = await screen.findByTestId("queued-message-indicator");
|
||||
expect(indicator).toBeInTheDocument();
|
||||
expect(indicator).toHaveTextContent("Queued");
|
||||
expect(indicator).toHaveTextContent("Queued message");
|
||||
|
||||
// Input should be cleared after queuing
|
||||
expect((input as HTMLTextAreaElement).value).toBe("");
|
||||
});
|
||||
|
||||
it("auto-sends queued message when agent response completes (AC4)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
|
||||
// Send first message
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Queue a second message while loading
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Auto-send this" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Verify it's queued
|
||||
expect(
|
||||
await screen.findByTestId("queued-message-indicator"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Simulate agent response completing (loading → false)
|
||||
act(() => {
|
||||
capturedWsHandlers?.onUpdate([
|
||||
{ role: "user", content: "First" },
|
||||
{ role: "assistant", content: "Done." },
|
||||
]);
|
||||
});
|
||||
|
||||
// The queued indicator should disappear (message was sent)
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("queued-message-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("cancel button discards the queued message (AC3, AC6)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
|
||||
// Send first message to start loading
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Queue a second message
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Discard me" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
const indicator = await screen.findByTestId("queued-message-indicator");
|
||||
expect(indicator).toBeInTheDocument();
|
||||
|
||||
// Click the ✕ cancel button
|
||||
const cancelBtn = screen.getByTitle("Cancel queued message");
|
||||
await act(async () => {
|
||||
fireEvent.click(cancelBtn);
|
||||
});
|
||||
|
||||
// Indicator should be gone
|
||||
expect(
|
||||
screen.queryByTestId("queued-message-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("edit button puts queued message back into input (AC3)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
|
||||
// Send first message to start loading
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Queue a second message
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Edit me back" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await screen.findByTestId("queued-message-indicator");
|
||||
|
||||
// Click the Edit button
|
||||
const editBtn = screen.getByTitle("Edit queued message");
|
||||
await act(async () => {
|
||||
fireEvent.click(editBtn);
|
||||
});
|
||||
|
||||
// Indicator should be gone and message back in input
|
||||
expect(
|
||||
screen.queryByTestId("queued-message-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
expect((input as HTMLTextAreaElement).value).toBe("Edit me back");
|
||||
});
|
||||
|
||||
it("subsequent submissions replace the queued message (AC5)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
|
||||
// Send first message to start loading
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Queue first replacement
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Original queue" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await screen.findByTestId("queued-message-indicator");
|
||||
|
||||
// Queue second replacement — should overwrite the first
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Replaced queue" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
const indicator = await screen.findByTestId("queued-message-indicator");
|
||||
expect(indicator).toHaveTextContent("Replaced queue");
|
||||
expect(indicator).not.toHaveTextContent("Original queue");
|
||||
});
|
||||
|
||||
it("does not auto-send queued message when generation is cancelled (AC6)", async () => {
|
||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||
|
||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||
|
||||
const input = screen.getByPlaceholderText("Send a message...");
|
||||
|
||||
// Send first message to start loading
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "First" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
// Queue a second message
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: "Should not send" } });
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||
});
|
||||
|
||||
await screen.findByTestId("queued-message-indicator");
|
||||
|
||||
// Click the stop button (■) — but input is empty so button is stop
|
||||
// Actually simulate cancel by clicking the stop button (which requires empty input)
|
||||
// We need to use the send button when input is empty (stop mode)
|
||||
// Simulate cancel via the cancelGeneration path: the button when loading && !input
|
||||
// At this point input is empty (was cleared after queuing)
|
||||
const stopButton = screen.getByRole("button", { name: "■" });
|
||||
await act(async () => {
|
||||
fireEvent.click(stopButton);
|
||||
});
|
||||
|
||||
// Queued indicator should be gone (cancelled)
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("queued-message-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user