story-kit: merge 160_story_constrain_thinking_trace_height_in_agent_stream_ui
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentConfigInfo, AgentInfo } from "../api/agents";
|
||||
import { agentsApi } from "../api/agents";
|
||||
import type { AgentConfigInfo, AgentEvent, AgentInfo } from "../api/agents";
|
||||
import { agentsApi, subscribeAgentStream } from "../api/agents";
|
||||
|
||||
vi.mock("../api/agents", () => {
|
||||
const agentsApi = {
|
||||
@@ -17,6 +17,8 @@ vi.mock("../api/agents", () => {
|
||||
// Dynamic import so the mock is in place before the module loads
|
||||
const { AgentPanel } = await import("./AgentPanel");
|
||||
|
||||
const mockedSubscribeAgentStream = vi.mocked(subscribeAgentStream);
|
||||
|
||||
const mockedAgents = {
|
||||
listAgents: vi.mocked(agentsApi.listAgents),
|
||||
getAgentConfig: vi.mocked(agentsApi.getAgentConfig),
|
||||
@@ -210,3 +212,214 @@ describe("RosterBadge availability state", () => {
|
||||
expect(dot.style.animation).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Thinking trace block in agent stream UI", () => {
|
||||
beforeAll(() => {
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockedAgents.getAgentConfig.mockResolvedValue(ROSTER);
|
||||
mockedAgents.listAgents.mockResolvedValue([]);
|
||||
mockedSubscribeAgentStream.mockReturnValue(() => {});
|
||||
});
|
||||
|
||||
// AC1+AC2: thinking block renders with fixed max-height and is visually distinct
|
||||
it("renders thinking block with max-height 96px when thinking event arrives", async () => {
|
||||
let emitEvent: ((e: AgentEvent) => void) | null = null;
|
||||
mockedSubscribeAgentStream.mockImplementation(
|
||||
(_storyId, _agentName, onEvent) => {
|
||||
emitEvent = onEvent;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "160_thinking",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: "master",
|
||||
log_session_id: null,
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(<AgentPanel />);
|
||||
|
||||
// Wait for the subscription to be set up
|
||||
await screen.findByTestId("roster-badge-coder-1");
|
||||
|
||||
// Fire a thinking event
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "thinking",
|
||||
story_id: "160_thinking",
|
||||
agent_name: "coder-1",
|
||||
text: "Let me consider the problem carefully...",
|
||||
});
|
||||
});
|
||||
|
||||
const block = screen.getByTestId("thinking-block");
|
||||
expect(block).toBeInTheDocument();
|
||||
// AC2: fixed max-height
|
||||
expect(block.style.maxHeight).toBe("96px");
|
||||
// AC2: overflow scrolling
|
||||
expect(block.style.overflowY).toBe("auto");
|
||||
// AC1: visually distinct — italic monospace font
|
||||
expect(block.style.fontStyle).toBe("italic");
|
||||
expect(block.style.fontFamily).toBe("monospace");
|
||||
// Contains the thinking text
|
||||
expect(block.textContent).toContain(
|
||||
"Let me consider the problem carefully...",
|
||||
);
|
||||
});
|
||||
|
||||
// AC3: thinking block renders the "thinking" label
|
||||
it("shows a thinking label in the block header", async () => {
|
||||
let emitEvent: ((e: AgentEvent) => void) | null = null;
|
||||
mockedSubscribeAgentStream.mockImplementation(
|
||||
(_storyId, _agentName, onEvent) => {
|
||||
emitEvent = onEvent;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "160_label",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: "master",
|
||||
log_session_id: null,
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(<AgentPanel />);
|
||||
await screen.findByTestId("roster-badge-coder-1");
|
||||
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "thinking",
|
||||
story_id: "160_label",
|
||||
agent_name: "coder-1",
|
||||
text: "thinking...",
|
||||
});
|
||||
});
|
||||
|
||||
const block = screen.getByTestId("thinking-block");
|
||||
expect(block.textContent).toContain("thinking");
|
||||
});
|
||||
|
||||
// AC4: regular text output renders outside the thinking container
|
||||
it("renders regular output outside the thinking block", async () => {
|
||||
let emitEvent: ((e: AgentEvent) => void) | null = null;
|
||||
mockedSubscribeAgentStream.mockImplementation(
|
||||
(_storyId, _agentName, onEvent) => {
|
||||
emitEvent = onEvent;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "160_output",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: "master",
|
||||
log_session_id: null,
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(<AgentPanel />);
|
||||
await screen.findByTestId("roster-badge-coder-1");
|
||||
|
||||
// First: thinking event
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "thinking",
|
||||
story_id: "160_output",
|
||||
agent_name: "coder-1",
|
||||
text: "thinking deeply",
|
||||
});
|
||||
});
|
||||
|
||||
// Then: text output event
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "output",
|
||||
story_id: "160_output",
|
||||
agent_name: "coder-1",
|
||||
text: "Here is the result.",
|
||||
});
|
||||
});
|
||||
|
||||
const thinkingBlock = screen.getByTestId("thinking-block");
|
||||
const outputArea = screen.getByTestId("agent-output-coder-1");
|
||||
|
||||
// Thinking still visible
|
||||
expect(thinkingBlock).toBeInTheDocument();
|
||||
expect(thinkingBlock.textContent).toContain("thinking deeply");
|
||||
|
||||
// Output renders in a separate element, not inside the thinking block
|
||||
expect(outputArea).toBeInTheDocument();
|
||||
expect(outputArea.textContent).toContain("Here is the result.");
|
||||
expect(thinkingBlock.contains(outputArea)).toBe(false);
|
||||
});
|
||||
|
||||
// AC5: thinking block remains visible when text starts
|
||||
it("keeps thinking block visible after output arrives", async () => {
|
||||
let emitEvent: ((e: AgentEvent) => void) | null = null;
|
||||
mockedSubscribeAgentStream.mockImplementation(
|
||||
(_storyId, _agentName, onEvent) => {
|
||||
emitEvent = onEvent;
|
||||
return () => {};
|
||||
},
|
||||
);
|
||||
|
||||
const agentList: AgentInfo[] = [
|
||||
{
|
||||
story_id: "160_persist",
|
||||
agent_name: "coder-1",
|
||||
status: "running",
|
||||
session_id: null,
|
||||
worktree_path: "/tmp/wt",
|
||||
base_branch: "master",
|
||||
log_session_id: null,
|
||||
},
|
||||
];
|
||||
mockedAgents.listAgents.mockResolvedValue(agentList);
|
||||
|
||||
render(<AgentPanel />);
|
||||
await screen.findByTestId("roster-badge-coder-1");
|
||||
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "thinking",
|
||||
story_id: "160_persist",
|
||||
agent_name: "coder-1",
|
||||
text: "my thoughts",
|
||||
});
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
emitEvent?.({
|
||||
type: "output",
|
||||
story_id: "160_persist",
|
||||
agent_name: "coder-1",
|
||||
text: "final answer",
|
||||
});
|
||||
});
|
||||
|
||||
// Thinking block still in the DOM after output arrives
|
||||
expect(screen.getByTestId("thinking-block")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user