story-kit: merge 155_story_queue_messages_while_agent_is_busy

This commit is contained in:
Dave
2026-02-24 16:29:05 +00:00
parent f0000e0436
commit bb1c3ac97c
2 changed files with 420 additions and 46 deletions

View File

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