mirror of
https://github.com/meta-llama/llama-stack.git
synced 2025-07-07 06:20:45 +00:00
feat(ui): add views for Responses (#2293)
# What does this PR do? * Add responses list and detail views * Refactored components to be shared as much as possible between chat completions and responses ## Test Plan <img width="2014" alt="image" src="https://github.com/user-attachments/assets/6dee12ea-8876-4351-a6eb-2338058466ef" /> <img width="2021" alt="image" src="https://github.com/user-attachments/assets/6c7c71b8-25b7-4199-9c57-6960be5580c8" /> added tests
This commit is contained in:
parent
6352078e4b
commit
56e5ddb39f
34 changed files with 3282 additions and 380 deletions
777
llama_stack/ui/components/responses/responses-detail.test.tsx
Normal file
777
llama_stack/ui/components/responses/responses-detail.test.tsx
Normal file
|
@ -0,0 +1,777 @@
|
|||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { ResponseDetailView } from "./responses-detail";
|
||||
import { OpenAIResponse, InputItemListResponse } from "@/lib/types";
|
||||
|
||||
describe("ResponseDetailView", () => {
|
||||
const defaultProps = {
|
||||
response: null,
|
||||
inputItems: null,
|
||||
isLoading: false,
|
||||
isLoadingInputItems: false,
|
||||
error: null,
|
||||
inputItemsError: null,
|
||||
id: "test_id",
|
||||
};
|
||||
|
||||
describe("Loading State", () => {
|
||||
test("renders loading skeleton when isLoading is true", () => {
|
||||
const { container } = render(
|
||||
<ResponseDetailView {...defaultProps} isLoading={true} />,
|
||||
);
|
||||
|
||||
// Check for skeleton elements
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
|
||||
// The title is replaced by a skeleton when loading, so we shouldn't expect the text
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error State", () => {
|
||||
test("renders error message when error prop is provided", () => {
|
||||
const errorMessage = "Network Error";
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
error={{ name: "Error", message: errorMessage }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Responses Details")).toBeInTheDocument();
|
||||
// The error message is split across elements, so we check for parts
|
||||
expect(
|
||||
screen.getByText(/Error loading details for ID/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/test_id/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Network Error/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders default error message when error.message is not available", () => {
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
error={{ name: "Error", message: "" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Error loading details for ID/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/test_id/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Not Found State", () => {
|
||||
test("renders not found message when response is null and not loading/error", () => {
|
||||
render(<ResponseDetailView {...defaultProps} response={null} />);
|
||||
|
||||
expect(screen.getByText("Responses Details")).toBeInTheDocument();
|
||||
// The message is split across elements
|
||||
expect(screen.getByText(/No details found for ID:/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/test_id/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response Data Rendering", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "llama-test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "Test response output",
|
||||
},
|
||||
],
|
||||
input: [
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: "Test input message",
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
top_p: 0.9,
|
||||
parallel_tool_calls: true,
|
||||
previous_response_id: "prev_resp_456",
|
||||
};
|
||||
|
||||
test("renders response data with input and output sections", () => {
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Check main sections
|
||||
expect(screen.getByText("Responses Details")).toBeInTheDocument();
|
||||
expect(screen.getByText("Input")).toBeInTheDocument();
|
||||
expect(screen.getByText("Output")).toBeInTheDocument();
|
||||
|
||||
// Check input content
|
||||
expect(screen.getByText("Test input message")).toBeInTheDocument();
|
||||
expect(screen.getByText("User")).toBeInTheDocument();
|
||||
|
||||
// Check output content
|
||||
expect(screen.getByText("Test response output")).toBeInTheDocument();
|
||||
expect(screen.getByText("Assistant")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders properties sidebar with all response metadata", () => {
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Check properties - use regex to handle text split across elements
|
||||
expect(screen.getByText(/Created/)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(new Date(1710000000 * 1000).toLocaleString()),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Check for the specific ID label (not Previous Response ID)
|
||||
expect(
|
||||
screen.getByText((content, element) => {
|
||||
return element?.tagName === "STRONG" && content === "ID:";
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("resp_123")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Model/)).toBeInTheDocument();
|
||||
expect(screen.getByText("llama-test-model")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Status/)).toBeInTheDocument();
|
||||
expect(screen.getByText("completed")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Temperature/)).toBeInTheDocument();
|
||||
expect(screen.getByText("0.7")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Top P/)).toBeInTheDocument();
|
||||
expect(screen.getByText("0.9")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Parallel Tool Calls/)).toBeInTheDocument();
|
||||
expect(screen.getByText("Yes")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Previous Response ID/)).toBeInTheDocument();
|
||||
expect(screen.getByText("prev_resp_456")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles optional properties correctly", () => {
|
||||
const minimalResponse: OpenAIResponse = {
|
||||
id: "resp_minimal",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(
|
||||
<ResponseDetailView {...defaultProps} response={minimalResponse} />,
|
||||
);
|
||||
|
||||
// Should show required properties
|
||||
expect(screen.getByText("resp_minimal")).toBeInTheDocument();
|
||||
expect(screen.getByText("test-model")).toBeInTheDocument();
|
||||
expect(screen.getByText("completed")).toBeInTheDocument();
|
||||
|
||||
// Should not show optional properties
|
||||
expect(screen.queryByText("Temperature")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Top P")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Parallel Tool Calls")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Previous Response ID"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders error information when response has error", () => {
|
||||
const errorResponse: OpenAIResponse = {
|
||||
...mockResponse,
|
||||
error: {
|
||||
code: "invalid_request",
|
||||
message: "The request was invalid",
|
||||
},
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={errorResponse} />);
|
||||
|
||||
// The error is shown in the properties sidebar, not as a separate "Error" label
|
||||
expect(
|
||||
screen.getByText("invalid_request: The request was invalid"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Input Items Handling", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [{ type: "message", role: "assistant", content: "output" }],
|
||||
input: [{ type: "message", role: "user", content: "fallback input" }],
|
||||
};
|
||||
|
||||
test("shows loading state for input items", () => {
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={mockResponse}
|
||||
isLoadingInputItems={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Check for skeleton loading in input items section
|
||||
const { container } = render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={mockResponse}
|
||||
isLoadingInputItems={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const skeletons = container.querySelectorAll('[data-slot="skeleton"]');
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("shows error message for input items with fallback", () => {
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={mockResponse}
|
||||
inputItemsError={{
|
||||
name: "Error",
|
||||
message: "Failed to load input items",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Error loading input items: Failed to load input items",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Falling back to response input data."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Should still show fallback input data
|
||||
expect(screen.getByText("fallback input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("uses input items data when available", () => {
|
||||
const mockInputItems: InputItemListResponse = {
|
||||
object: "list",
|
||||
data: [
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: "input from items API",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={mockResponse}
|
||||
inputItems={mockInputItems}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should show input items data, not response.input
|
||||
expect(screen.getByText("input from items API")).toBeInTheDocument();
|
||||
expect(screen.queryByText("fallback input")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("falls back to response.input when input items is empty", () => {
|
||||
const emptyInputItems: InputItemListResponse = {
|
||||
object: "list",
|
||||
data: [],
|
||||
};
|
||||
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={mockResponse}
|
||||
inputItems={emptyInputItems}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Should show fallback input data
|
||||
expect(screen.getByText("fallback input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows no input message when no data available", () => {
|
||||
const responseWithoutInput: OpenAIResponse = {
|
||||
...mockResponse,
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(
|
||||
<ResponseDetailView
|
||||
{...defaultProps}
|
||||
response={responseWithoutInput}
|
||||
inputItems={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("No input data available.")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Input Display Components", () => {
|
||||
test("renders string content input correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: "Simple string input",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("Simple string input")).toBeInTheDocument();
|
||||
expect(screen.getByText("User")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders array content input correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "input_text", text: "First part" },
|
||||
{ type: "output_text", text: "Second part" },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("First part Second part")).toBeInTheDocument();
|
||||
expect(screen.getByText("User")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders non-message input types correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "function_call",
|
||||
content: "function call content",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("function call content")).toBeInTheDocument();
|
||||
// Use getAllByText to find the specific "Input" with the type detail
|
||||
const inputElements = screen.getAllByText("Input");
|
||||
expect(inputElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("(function_call)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("handles input with object content", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "custom_type",
|
||||
content: JSON.stringify({ key: "value", nested: { data: "test" } }),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Should show JSON stringified content (without quotes around keys in the rendered output)
|
||||
expect(screen.getByText(/key.*value/)).toBeInTheDocument();
|
||||
// Use getAllByText to find the specific "Input" with the type detail
|
||||
const inputElements = screen.getAllByText("Input");
|
||||
expect(inputElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("(custom_type)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders function call input correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "function_call",
|
||||
id: "call_456",
|
||||
status: "completed",
|
||||
name: "input_function",
|
||||
arguments: '{"param": "value"}',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('input_function({"param": "value"})'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Function Call")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders web search call input correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "web_search_call",
|
||||
id: "search_789",
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("web_search_call(status: completed)"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Function Call")).toBeInTheDocument();
|
||||
expect(screen.getByText("(Web Search)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Output Display Components", () => {
|
||||
test("renders message output with string content", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: "Simple string output",
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("Simple string output")).toBeInTheDocument();
|
||||
expect(screen.getByText("Assistant")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders message output with array content", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "output_text", text: "First output" },
|
||||
{ type: "input_text", text: "Second output" },
|
||||
],
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("First output Second output"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Assistant")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders function call output correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "function_call",
|
||||
id: "call_123",
|
||||
status: "completed",
|
||||
name: "search_function",
|
||||
arguments: '{"query": "test"}',
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('search_function({"query": "test"})'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Function Call")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders function call output without arguments", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "function_call",
|
||||
id: "call_123",
|
||||
status: "completed",
|
||||
name: "simple_function",
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("simple_function({})")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Function Call/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders web search call output correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "web_search_call",
|
||||
id: "search_123",
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("web_search_call(status: completed)"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText(/Function Call/)).toBeInTheDocument();
|
||||
expect(screen.getByText("(Web Search)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders unknown output types with JSON fallback", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "unknown_type",
|
||||
custom_field: "custom_value",
|
||||
data: { nested: "object" },
|
||||
} as any,
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Should show JSON stringified content
|
||||
expect(
|
||||
screen.getByText(/custom_field.*custom_value/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("(unknown_type)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("shows no output message when output array is empty", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("No output data available.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("groups function call with its output correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "function_call",
|
||||
id: "call_123",
|
||||
status: "completed",
|
||||
name: "get_weather",
|
||||
arguments: '{"city": "Tokyo"}',
|
||||
},
|
||||
{
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
call_id: "call_123",
|
||||
content: "sunny and warm",
|
||||
} as any, // Using any to bypass the type restriction for this test
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Should show the function call and message as separate items (not grouped)
|
||||
expect(screen.getByText("Function Call")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('get_weather({"city": "Tokyo"})'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Assistant")).toBeInTheDocument();
|
||||
expect(screen.getByText("sunny and warm")).toBeInTheDocument();
|
||||
|
||||
// Should NOT have the grouped "Arguments" and "Output" labels
|
||||
expect(screen.queryByText("Arguments")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("groups function call with function_call_output correctly", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "function_call",
|
||||
call_id: "call_123",
|
||||
status: "completed",
|
||||
name: "get_weather",
|
||||
arguments: '{"city": "Tokyo"}',
|
||||
},
|
||||
{
|
||||
type: "function_call_output",
|
||||
id: "fc_68364957013081...",
|
||||
status: "completed",
|
||||
call_id: "call_123",
|
||||
output: "sunny and warm",
|
||||
} as any, // Using any to bypass the type restriction for this test
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// Should show the function call grouped with its clean output
|
||||
expect(screen.getByText("Function Call")).toBeInTheDocument();
|
||||
expect(screen.getByText("Arguments")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('get_weather({"city": "Tokyo"})'),
|
||||
).toBeInTheDocument();
|
||||
// Use getAllByText since there are multiple "Output" elements (card title and output label)
|
||||
const outputElements = screen.getAllByText("Output");
|
||||
expect(outputElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("sunny and warm")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases and Error Handling", () => {
|
||||
test("handles missing role in message input", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [],
|
||||
input: [
|
||||
{
|
||||
type: "message",
|
||||
content: "Message without role",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
expect(screen.getByText("Message without role")).toBeInTheDocument();
|
||||
expect(screen.getByText("Unknown")).toBeInTheDocument(); // Default role
|
||||
});
|
||||
|
||||
test("handles missing name in function call output", () => {
|
||||
const mockResponse: OpenAIResponse = {
|
||||
id: "resp_123",
|
||||
object: "response",
|
||||
created_at: 1710000000,
|
||||
model: "test-model",
|
||||
status: "completed",
|
||||
output: [
|
||||
{
|
||||
type: "function_call",
|
||||
id: "call_123",
|
||||
status: "completed",
|
||||
},
|
||||
],
|
||||
input: [],
|
||||
};
|
||||
|
||||
render(<ResponseDetailView {...defaultProps} response={mockResponse} />);
|
||||
|
||||
// When name is missing, it falls back to JSON.stringify of the entire output
|
||||
const functionCallElements = screen.getAllByText(/function_call/);
|
||||
expect(functionCallElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText(/call_123/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue