mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-10-04 04:04:14 +00:00
# What does this PR do? - Introduces the Agent Session creation for the Playground and allows users to set tools - note tools are actually not usable yet and this is marked explicitly - this also caches sessions locally for faster loading on the UI and deletes them appropriately - allows users to easily create new sessions as well - Moved Model Configuration settings and "System Message" / Prompt to the left component - Added new logo and favicon - Added new typing animation when LLM is generating ### Create New Session <img width="1916" height="1393" alt="Screenshot 2025-08-21 at 4 18 08 PM" src="https://github.com/user-attachments/assets/52c70ae3-a33e-4338-8522-8184c692c320" /> ### List of Sessions <img width="1920" height="1391" alt="Screenshot 2025-08-21 at 4 18 56 PM" src="https://github.com/user-attachments/assets/ed78c3c6-08ec-486c-8bad-9b7382c11360" /> <!-- If resolving an issue, uncomment and update the line below --> <!-- Closes #[issue-number] --> ## Test Plan Unit tests added --------- Signed-off-by: Francisco Javier Arceo <farceo@redhat.com>
345 lines
9.3 KiB
TypeScript
345 lines
9.3 KiB
TypeScript
import React from "react";
|
|
import { render, screen, waitFor, act } from "@testing-library/react";
|
|
import "@testing-library/jest-dom";
|
|
import { Conversations, SessionUtils } from "./conversations";
|
|
import type { Message } from "@/components/chat-playground/chat-message";
|
|
|
|
interface ChatSession {
|
|
id: string;
|
|
name: string;
|
|
messages: Message[];
|
|
selectedModel: string;
|
|
systemMessage: string;
|
|
agentId: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
const mockOnSessionChange = jest.fn();
|
|
const mockOnNewSession = jest.fn();
|
|
|
|
// Mock the auth client
|
|
const mockClient = {
|
|
agents: {
|
|
session: {
|
|
list: jest.fn(),
|
|
create: jest.fn(),
|
|
delete: jest.fn(),
|
|
retrieve: jest.fn(),
|
|
},
|
|
},
|
|
};
|
|
|
|
// Mock the useAuthClient hook
|
|
jest.mock("@/hooks/use-auth-client", () => ({
|
|
useAuthClient: jest.fn(() => mockClient),
|
|
}));
|
|
|
|
// Mock additional SessionUtils methods that are now being used
|
|
jest.mock("./conversations", () => {
|
|
const actual = jest.requireActual("./conversations");
|
|
return {
|
|
...actual,
|
|
SessionUtils: {
|
|
...actual.SessionUtils,
|
|
saveSessionData: jest.fn(),
|
|
loadSessionData: jest.fn(),
|
|
saveAgentConfig: jest.fn(),
|
|
loadAgentConfig: jest.fn(),
|
|
clearAgentCache: jest.fn(),
|
|
},
|
|
};
|
|
});
|
|
|
|
const localStorageMock = {
|
|
getItem: jest.fn(),
|
|
setItem: jest.fn(),
|
|
removeItem: jest.fn(),
|
|
clear: jest.fn(),
|
|
};
|
|
|
|
Object.defineProperty(window, "localStorage", {
|
|
value: localStorageMock,
|
|
writable: true,
|
|
});
|
|
|
|
// Mock crypto.randomUUID for test environment
|
|
let uuidCounter = 0;
|
|
Object.defineProperty(globalThis, "crypto", {
|
|
value: {
|
|
randomUUID: jest.fn(() => `test-uuid-${++uuidCounter}`),
|
|
},
|
|
writable: true,
|
|
});
|
|
|
|
describe("SessionManager", () => {
|
|
const mockSession: ChatSession = {
|
|
id: "session_123",
|
|
name: "Test Session",
|
|
messages: [
|
|
{
|
|
id: "msg_1",
|
|
role: "user",
|
|
content: "Hello",
|
|
createdAt: new Date(),
|
|
},
|
|
],
|
|
selectedModel: "test-model",
|
|
systemMessage: "You are a helpful assistant.",
|
|
agentId: "agent_123",
|
|
createdAt: 1710000000,
|
|
updatedAt: 1710001000,
|
|
};
|
|
|
|
const mockAgentSessions = [
|
|
{
|
|
session_id: "session_123",
|
|
session_name: "Test Session",
|
|
started_at: "2024-01-01T00:00:00Z",
|
|
turns: [],
|
|
},
|
|
{
|
|
session_id: "session_456",
|
|
session_name: "Another Session",
|
|
started_at: "2024-01-01T01:00:00Z",
|
|
turns: [],
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
localStorageMock.getItem.mockReturnValue(null);
|
|
localStorageMock.setItem.mockImplementation(() => {});
|
|
mockClient.agents.session.list.mockResolvedValue({
|
|
data: mockAgentSessions,
|
|
});
|
|
mockClient.agents.session.create.mockResolvedValue({
|
|
session_id: "new_session_123",
|
|
});
|
|
mockClient.agents.session.delete.mockResolvedValue(undefined);
|
|
mockClient.agents.session.retrieve.mockResolvedValue({
|
|
session_id: "test-session",
|
|
session_name: "Test Session",
|
|
started_at: new Date().toISOString(),
|
|
turns: [],
|
|
});
|
|
uuidCounter = 0; // Reset UUID counter for consistent test behavior
|
|
});
|
|
|
|
describe("Component Rendering", () => {
|
|
test("does not render when no agent is selected", async () => {
|
|
const { container } = await act(async () => {
|
|
return render(
|
|
<Conversations
|
|
selectedAgentId=""
|
|
currentSession={null}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
test("renders loading state initially", async () => {
|
|
mockClient.agents.session.list.mockImplementation(
|
|
() => new Promise(() => {}) // Never resolves to simulate loading
|
|
);
|
|
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={null}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
expect(screen.getByText("Select Session")).toBeInTheDocument();
|
|
// When loading, the "+ New" button should be disabled
|
|
expect(screen.getByText("+ New")).toBeDisabled();
|
|
});
|
|
|
|
test("renders session selector when agent sessions are loaded", async () => {
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={null}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Select Session")).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
test("renders current session name when session is selected", async () => {
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={mockSession}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Test Session")).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Agent API Integration", () => {
|
|
test("loads sessions from agent API on mount", async () => {
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={mockSession}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(mockClient.agents.session.list).toHaveBeenCalledWith(
|
|
"agent_123"
|
|
);
|
|
});
|
|
});
|
|
|
|
test("handles API errors gracefully", async () => {
|
|
mockClient.agents.session.list.mockRejectedValue(new Error("API Error"));
|
|
const consoleSpy = jest
|
|
.spyOn(console, "error")
|
|
.mockImplementation(() => {});
|
|
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={mockSession}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
"Error loading agent sessions:",
|
|
expect.any(Error)
|
|
);
|
|
});
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("Error Handling", () => {
|
|
test("component renders without crashing when API is unavailable", async () => {
|
|
mockClient.agents.session.list.mockRejectedValue(
|
|
new Error("Network Error")
|
|
);
|
|
const consoleSpy = jest
|
|
.spyOn(console, "error")
|
|
.mockImplementation(() => {});
|
|
|
|
await act(async () => {
|
|
render(
|
|
<Conversations
|
|
selectedAgentId="agent_123"
|
|
currentSession={mockSession}
|
|
onSessionChange={mockOnSessionChange}
|
|
onNewSession={mockOnNewSession}
|
|
/>
|
|
);
|
|
});
|
|
|
|
// Should still render the session manager with the select trigger
|
|
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
|
expect(screen.getByText("+ New")).toBeInTheDocument();
|
|
consoleSpy.mockRestore();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("SessionUtils", () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
localStorageMock.getItem.mockReturnValue(null);
|
|
localStorageMock.setItem.mockImplementation(() => {});
|
|
});
|
|
|
|
describe("saveCurrentSessionId", () => {
|
|
test("saves session ID to localStorage", () => {
|
|
SessionUtils.saveCurrentSessionId("test-session-id");
|
|
|
|
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
|
"chat-playground-current-session",
|
|
"test-session-id"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("createDefaultSession", () => {
|
|
test("creates default session with agent ID", () => {
|
|
const result = SessionUtils.createDefaultSession("agent_123");
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
name: "Default Session",
|
|
messages: [],
|
|
selectedModel: "",
|
|
systemMessage: "You are a helpful assistant.",
|
|
agentId: "agent_123",
|
|
})
|
|
);
|
|
expect(result.id).toBeTruthy();
|
|
expect(result.createdAt).toBeTruthy();
|
|
expect(result.updatedAt).toBeTruthy();
|
|
});
|
|
|
|
test("creates default session with inherited model", () => {
|
|
const result = SessionUtils.createDefaultSession(
|
|
"agent_123",
|
|
"inherited-model"
|
|
);
|
|
|
|
expect(result.selectedModel).toBe("inherited-model");
|
|
expect(result.agentId).toBe("agent_123");
|
|
});
|
|
|
|
test("creates unique session IDs", () => {
|
|
const originalNow = Date.now;
|
|
let mockTime = 1710005000;
|
|
Date.now = jest.fn(() => ++mockTime);
|
|
|
|
const session1 = SessionUtils.createDefaultSession("agent_123");
|
|
const session2 = SessionUtils.createDefaultSession("agent_123");
|
|
|
|
expect(session1.id).not.toBe(session2.id);
|
|
|
|
Date.now = originalNow;
|
|
});
|
|
|
|
test("sets creation and update timestamps", () => {
|
|
const result = SessionUtils.createDefaultSession("agent_123");
|
|
|
|
expect(result.createdAt).toBeTruthy();
|
|
expect(result.updatedAt).toBeTruthy();
|
|
expect(typeof result.createdAt).toBe("number");
|
|
expect(typeof result.updatedAt).toBe("number");
|
|
});
|
|
});
|
|
});
|