story-kit: accept 145_story_persist_chat_history_to_localstorage_across_rebuilds

This commit is contained in:
Dave
2026-02-24 17:03:04 +00:00
parent aef022c74c
commit 6f7338dfdb
5 changed files with 324 additions and 6 deletions

View File

@@ -6,7 +6,7 @@ import {
waitFor,
} from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "../api/client";
import type { Message } from "../types";
import { Chat } from "./Chat";
@@ -395,6 +395,103 @@ describe("Chat reconciliation banner", () => {
});
});
describe("Chat localStorage persistence (Story 145)", () => {
const PROJECT_PATH = "/tmp/project";
const STORAGE_KEY = `storykit-chat-history:${PROJECT_PATH}`;
beforeEach(() => {
capturedWsHandlers = null;
localStorage.clear();
setupMocks();
});
afterEach(() => {
localStorage.clear();
});
it("AC1: restores persisted messages on mount", async () => {
const saved: Message[] = [
{ role: "user", content: "Previously saved question" },
{ role: "assistant", content: "Previously saved answer" },
];
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
expect(
await screen.findByText("Previously saved question"),
).toBeInTheDocument();
expect(
await screen.findByText("Previously saved answer"),
).toBeInTheDocument();
});
it("AC2: persists messages when WebSocket onUpdate fires", async () => {
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
const history: Message[] = [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there!" },
];
act(() => {
capturedWsHandlers?.onUpdate(history);
});
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
expect(stored).toEqual(history);
});
it("AC3: clears localStorage when New Session is clicked", async () => {
const saved: Message[] = [
{ role: "user", content: "Old message" },
{ role: "assistant", content: "Old reply" },
];
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
// Stub window.confirm to auto-approve the clear dialog
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
// Wait for the persisted message to appear
expect(await screen.findByText("Old message")).toBeInTheDocument();
// Click "New Session" button
const newSessionBtn = screen.getByText(/New Session/);
await act(async () => {
fireEvent.click(newSessionBtn);
});
// localStorage should be cleared
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
// Messages should be gone from the UI
expect(screen.queryByText("Old message")).not.toBeInTheDocument();
confirmSpy.mockRestore();
});
it("AC5: uses project-scoped storage key", async () => {
const otherKey = "storykit-chat-history:/other/project";
localStorage.setItem(
otherKey,
JSON.stringify([{ role: "user", content: "Other project msg" }]),
);
render(<Chat projectPath={PROJECT_PATH} onCloseProject={vi.fn()} />);
// Should NOT show the other project's messages
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
expect(screen.queryByText("Other project msg")).not.toBeInTheDocument();
// Other project's data should still be in storage
expect(localStorage.getItem(otherKey)).not.toBeNull();
});
});
describe("Chat activity status indicator (Bug 140)", () => {
beforeEach(() => {
capturedWsHandlers = null;

View File

@@ -4,6 +4,7 @@ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import type { PipelineState } from "../api/client";
import { api, ChatWebSocket } from "../api/client";
import { useChatHistory } from "../hooks/useChatHistory";
import type { Message, ProviderConfig, ToolCall } from "../types";
import { AgentPanel } from "./AgentPanel";
import { ChatHeader } from "./ChatHeader";
@@ -55,7 +56,7 @@ interface ChatProps {
}
export function Chat({ projectPath, onCloseProject }: ChatProps) {
const [messages, setMessages] = useState<Message[]>([]);
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const [model, setModel] = useState("llama3.1");
@@ -458,7 +459,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
console.error("Failed to cancel chat:", e);
}
setMessages([]);
clearMessages();
setStreamingContent("");
setLoading(false);
setActivityStatus(null);