feat(FN-1549): simplify chat creation with agent-only selection and auto title
- Remove task selection from chat creation flow - Add agent-only chat creation with automatic title generation - Introduce useChat hook for unified chat state management - Update ChatView with simplified creation UI - Add CSS styles for chat creation interface - Add comprehensive tests for chat manager and routes - Update routes to support agent-only chat creation
This commit is contained in:
@@ -6,6 +6,17 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { ChatManager, __setCreateKbAgent, __resetChatState } from "../chat.js";
|
||||
|
||||
// ── Mock Setup ──────────────────────────────────────────────────────────────
|
||||
|
||||
// Mock summarizeTitle using vi.hoisted so it's available at module hoisting time
|
||||
const { mockSummarizeTitle } = vi.hoisted(() => ({
|
||||
mockSummarizeTitle: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
summarizeTitle: mockSummarizeTitle,
|
||||
}));
|
||||
|
||||
// ── Mock Store ──────────────────────────────────────────────────────────────
|
||||
|
||||
const mockChatStore = {
|
||||
@@ -13,6 +24,7 @@ const mockChatStore = {
|
||||
createSession: vi.fn(),
|
||||
addMessage: vi.fn(),
|
||||
getMessages: vi.fn(),
|
||||
updateSession: vi.fn(),
|
||||
};
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
@@ -251,4 +263,110 @@ describe("ChatManager.sendMessage", () => {
|
||||
expect(calls[1][0]).toBe("chat-001");
|
||||
expect(calls[1][1].role).toBe("assistant");
|
||||
});
|
||||
|
||||
it("generates title when session has no title", async () => {
|
||||
mockSummarizeTitle.mockResolvedValue("Short Title");
|
||||
|
||||
__setCreateKbAgent(async (options: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
if (options.onText) options.onText("Response");
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
state: { messages: [] },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const chatManager = new ChatManager(
|
||||
mockChatStore as any,
|
||||
"/tmp/test",
|
||||
);
|
||||
|
||||
await chatManager.sendMessage("chat-001", "This is a long message that needs to be summarized");
|
||||
|
||||
// Wait for the async title generation
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Assert - summarizeTitle was called with the message content and model params
|
||||
expect(mockSummarizeTitle).toHaveBeenCalledWith(
|
||||
"This is a long message that needs to be summarized",
|
||||
"/tmp/test",
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Assert - session was updated with the generated title
|
||||
expect(mockChatStore.updateSession).toHaveBeenCalledWith("chat-001", { title: "Short Title" });
|
||||
});
|
||||
|
||||
it("uses truncated content when summarizeTitle returns null", async () => {
|
||||
mockSummarizeTitle.mockResolvedValue(null);
|
||||
|
||||
__setCreateKbAgent(async (options: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
if (options.onText) options.onText("Response");
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
state: { messages: [] },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const chatManager = new ChatManager(
|
||||
mockChatStore as any,
|
||||
"/tmp/test",
|
||||
);
|
||||
|
||||
const longMessage = "A".repeat(300);
|
||||
await chatManager.sendMessage("chat-001", longMessage);
|
||||
|
||||
// Wait for the async title generation
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Assert - summarizeTitle was called
|
||||
expect(mockSummarizeTitle).toHaveBeenCalled();
|
||||
|
||||
// Assert - session was updated with truncated content (first 60 chars)
|
||||
expect(mockChatStore.updateSession).toHaveBeenCalledWith("chat-001", { title: "A".repeat(60) });
|
||||
});
|
||||
|
||||
it("does not generate title when session already has a title", async () => {
|
||||
mockChatStore.getSession.mockReturnValue({
|
||||
id: "chat-001",
|
||||
agentId: "agent-001",
|
||||
status: "active",
|
||||
title: "Existing Title",
|
||||
});
|
||||
|
||||
__setCreateKbAgent(async (options: any) => {
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
if (options.onText) options.onText("Response");
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
state: { messages: [] },
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const chatManager = new ChatManager(
|
||||
mockChatStore as any,
|
||||
"/tmp/test",
|
||||
);
|
||||
|
||||
await chatManager.sendMessage("chat-001", "This is a long message");
|
||||
|
||||
// Wait for potential async operations
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Assert - summarizeTitle was NOT called
|
||||
expect(mockSummarizeTitle).not.toHaveBeenCalled();
|
||||
// Assert - updateSession was NOT called
|
||||
expect(mockChatStore.updateSession).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +84,10 @@ const mockAddMessage = vi.fn();
|
||||
const mockGetMessages = vi.fn();
|
||||
const mockGetMessage = vi.fn();
|
||||
|
||||
// Mock AgentStore
|
||||
const mockAgentStoreInit = vi.fn().mockResolvedValue(undefined);
|
||||
const mockAgentStoreGetAgent = vi.fn();
|
||||
|
||||
// Mock ChatStore class for vi.mock
|
||||
vi.mock("@fusion/core", () => {
|
||||
return {
|
||||
@@ -98,6 +102,10 @@ vi.mock("@fusion/core", () => {
|
||||
getMessages = mockGetMessages;
|
||||
getMessage = mockGetMessage;
|
||||
},
|
||||
AgentStore: class MockAgentStore {
|
||||
init = mockAgentStoreInit;
|
||||
getAgent = mockAgentStoreGetAgent;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -238,11 +246,27 @@ describe("Chat API Routes", () => {
|
||||
mockGetMessages.mockReset();
|
||||
mockGetMessage.mockReset();
|
||||
mockSendMessage.mockReset();
|
||||
mockAgentStoreInit.mockResolvedValue(undefined);
|
||||
mockAgentStoreGetAgent.mockReset();
|
||||
|
||||
// Setup default mocks
|
||||
mockListSessions.mockReturnValue([]);
|
||||
mockGetMessages.mockReturnValue([]);
|
||||
|
||||
// Default agent mock - agent with model config
|
||||
mockAgentStoreGetAgent.mockResolvedValue({
|
||||
id: "agent-001",
|
||||
name: "Alpha",
|
||||
role: "executor",
|
||||
state: "idle",
|
||||
createdAt: "2026-04-08T00:00:00.000Z",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
metadata: {},
|
||||
runtimeConfig: {
|
||||
model: "anthropic/claude-sonnet-4-5",
|
||||
},
|
||||
});
|
||||
|
||||
store = new MockStore();
|
||||
// Reset and use the shared mock instance
|
||||
mockChatStore = mockChatStoreInstance;
|
||||
@@ -319,7 +343,7 @@ describe("Chat API Routes", () => {
|
||||
});
|
||||
|
||||
describe("POST /api/chat/sessions", () => {
|
||||
it("creates session with required fields", async () => {
|
||||
it("creates session with required fields and resolves model from agent config", async () => {
|
||||
mockCreateSession.mockReturnValue(sampleSession);
|
||||
|
||||
const response = await request(
|
||||
@@ -332,15 +356,16 @@ describe("Chat API Routes", () => {
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect((response.body as any).session.id).toBe("chat-abc123");
|
||||
// Model is resolved from agent's runtimeConfig.model
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
agentId: "agent-001",
|
||||
title: null,
|
||||
modelProvider: null,
|
||||
modelId: null,
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("creates session with optional fields", async () => {
|
||||
it("creates session with title and resolves model from agent config", async () => {
|
||||
const sessionWithOptions = {
|
||||
...sampleSession,
|
||||
title: "Custom Title",
|
||||
@@ -356,8 +381,6 @@ describe("Chat API Routes", () => {
|
||||
JSON.stringify({
|
||||
agentId: "agent-001",
|
||||
title: "Custom Title",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
@@ -392,36 +415,52 @@ describe("Chat API Routes", () => {
|
||||
expect((response.body as any).error).toContain("agentId is required");
|
||||
});
|
||||
|
||||
it("returns 400 when modelProvider without modelId", async () => {
|
||||
it("returns 404 when agent not found", async () => {
|
||||
mockAgentStoreGetAgent.mockResolvedValueOnce(undefined);
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/chat/sessions",
|
||||
JSON.stringify({
|
||||
agentId: "agent-001",
|
||||
modelProvider: "anthropic",
|
||||
}),
|
||||
JSON.stringify({ agentId: "nonexistent" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((response.body as any).error).toContain("both be provided or neither");
|
||||
expect(response.status).toBe(404);
|
||||
expect((response.body as any).error).toContain("not found");
|
||||
});
|
||||
|
||||
it("returns 400 when modelId without modelProvider", async () => {
|
||||
it("creates session with default model when agent has no model config", async () => {
|
||||
mockAgentStoreGetAgent.mockResolvedValueOnce({
|
||||
id: "agent-002",
|
||||
name: "Beta",
|
||||
role: "reviewer",
|
||||
state: "idle",
|
||||
createdAt: "2026-04-08T00:00:00.000Z",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
metadata: {},
|
||||
runtimeConfig: {},
|
||||
});
|
||||
|
||||
const sessionNoModel = { ...sampleSession, agentId: "agent-002" };
|
||||
mockCreateSession.mockReturnValue(sessionNoModel);
|
||||
|
||||
const response = await request(
|
||||
app,
|
||||
"POST",
|
||||
"/api/chat/sessions",
|
||||
JSON.stringify({
|
||||
agentId: "agent-001",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
JSON.stringify({ agentId: "agent-002" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((response.body as any).error).toContain("both be provided or neither");
|
||||
expect(response.status).toBe(201);
|
||||
// No model resolved from agent config
|
||||
expect(mockCreateSession).toHaveBeenCalledWith({
|
||||
agentId: "agent-002",
|
||||
title: null,
|
||||
modelProvider: null,
|
||||
modelId: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user