fix(frontend): deduplicate live workflow activity
This commit is contained in:
@@ -3,12 +3,13 @@ import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, vi } from "vitest";
|
||||
import type { ActivityEntry, ActivityKind } from "../api/types";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { isNearBottom } from "./activityScroll";
|
||||
import * as activityPanelModule from "./ModelActivityPanel";
|
||||
import { isNearBottom, ModelActivityPanel } from "./ModelActivityPanel";
|
||||
import { ModelActivityPanel } from "./ModelActivityPanel";
|
||||
|
||||
beforeEach(() => useSessionStore.getState().resetSession());
|
||||
|
||||
test("renders only prompt, thinking, status, and gate from a mixed F1 sequence", () => {
|
||||
test("renders only thinking and status from a mixed F1 sequence", () => {
|
||||
const store = useSessionStore.getState();
|
||||
store.setPhase("F1");
|
||||
store.setLastUserEntry({ kind: "input", text: "How many patients?" });
|
||||
@@ -17,10 +18,6 @@ test("renders only prompt, thinking, status, and gate from a mixed F1 sequence",
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "running" },
|
||||
});
|
||||
store.applyEvent({
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-1", toolName: "bash", status: "completed" },
|
||||
});
|
||||
store.applyEvent({ type: "text_delta", text: "Let me run another bash search." });
|
||||
store.applyEvent({ type: "activity_delta", text: "Inspecting **schema**." });
|
||||
store.applyEvent({ type: "info", level: "warning", text: "Retrying schema lookup" });
|
||||
@@ -31,14 +28,12 @@ test("renders only prompt, thinking, status, and gate from a mixed F1 sequence",
|
||||
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
|
||||
expect(screen.getAllByRole("article")).toHaveLength(4);
|
||||
expect(screen.getByText("How many patients?")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("article")).toHaveLength(2);
|
||||
expect(screen.getByText("schema").tagName).toBe("STRONG");
|
||||
expect(screen.getByText("Retrying schema lookup")).toBeInTheDocument();
|
||||
expect(screen.getByText("Confirm cohort")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Agent started")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("How many patients?")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Confirm cohort")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("bash")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("completed")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Let me run another bash search.")).not.toBeInTheDocument();
|
||||
expect(useSessionStore.getState().transcript).toEqual([
|
||||
{ role: "assistant", text: "Let me run another bash search." },
|
||||
@@ -52,10 +47,10 @@ test("uses an explicit default-deny activity-kind allowlist", () => {
|
||||
expect(predicate).toBeTypeOf("function");
|
||||
if (!predicate) return;
|
||||
|
||||
for (const kind of ["prompt", "thinking", "status", "gate"] satisfies ActivityKind[]) {
|
||||
for (const kind of ["thinking", "status"] satisfies ActivityKind[]) {
|
||||
expect(predicate({ kind, phase: "F1", text: kind })).toBe(true);
|
||||
}
|
||||
for (const kind of ["assistant", "tool", "lifecycle"] satisfies ActivityKind[]) {
|
||||
for (const kind of ["prompt", "gate", "assistant", "tool", "lifecycle"] satisfies ActivityKind[]) {
|
||||
expect(predicate({ kind, phase: "F1", text: kind })).toBe(false);
|
||||
}
|
||||
expect(predicate({ kind: "future-kind" as ActivityKind, phase: "F1", text: "future" })).toBe(false);
|
||||
@@ -64,6 +59,8 @@ test("uses an explicit default-deny activity-kind allowlist", () => {
|
||||
test("shows the empty state when the raw log contains only hidden entries", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [
|
||||
{ kind: "prompt", phase: "F1", text: "How many patients?" },
|
||||
{ kind: "gate", phase: "F1", text: "Confirm cohort" },
|
||||
{ kind: "assistant", phase: "F1", text: "Let me try another command." },
|
||||
{ kind: "tool", phase: "F1", text: "bash", toolCallId: "tool-1", status: "completed" },
|
||||
{ kind: "lifecycle", phase: "F1", text: "Turn end" },
|
||||
@@ -122,7 +119,7 @@ test("renders thinking markdown without assistant narration", () => {
|
||||
test("close calls onClose without mutating the activity log", async () => {
|
||||
const onClose = vi.fn();
|
||||
const activityLog: ActivityEntry[] = [
|
||||
{ kind: "prompt", phase: "F1", text: "Keep this row" },
|
||||
{ kind: "status", phase: "F1", text: "Keep this row" },
|
||||
];
|
||||
useSessionStore.setState({ activityLog });
|
||||
render(<ModelActivityPanel onClose={onClose} />);
|
||||
@@ -151,9 +148,9 @@ function setScrollGeometry(
|
||||
});
|
||||
}
|
||||
|
||||
test("follows appended activity while the viewport is near the bottom", () => {
|
||||
test("follows appended visible activity while near the bottom", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [{ kind: "prompt", phase: "F1", text: "First" }],
|
||||
activityLog: [{ kind: "thinking", phase: "F1", text: "First" }],
|
||||
});
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
const viewport = screen.getByTestId("activity-scroll");
|
||||
@@ -165,9 +162,9 @@ test("follows appended activity while the viewport is near the bottom", () => {
|
||||
expect(viewport).toHaveAttribute("tabindex", "0");
|
||||
});
|
||||
|
||||
test("preserves manual scroll position when appended activity arrives", () => {
|
||||
test("preserves manual scroll position when visible activity arrives", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [{ kind: "prompt", phase: "F1", text: "First" }],
|
||||
activityLog: [{ kind: "thinking", phase: "F1", text: "First" }],
|
||||
});
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
const viewport = screen.getByTestId("activity-scroll");
|
||||
@@ -179,18 +176,15 @@ test("preserves manual scroll position when appended activity arrives", () => {
|
||||
expect(viewport.scrollTop).toBe(100);
|
||||
});
|
||||
|
||||
test("does not bottom-follow when only a hidden event arrives", () => {
|
||||
test("does not bottom-follow when only a hidden prompt arrives", () => {
|
||||
useSessionStore.setState({
|
||||
activityLog: [{ kind: "prompt", phase: "F1", text: "Visible prompt" }],
|
||||
activityLog: [{ kind: "status", phase: "F1", text: "Visible status" }],
|
||||
});
|
||||
render(<ModelActivityPanel onClose={vi.fn()} />);
|
||||
const viewport = screen.getByTestId("activity-scroll");
|
||||
setScrollGeometry(viewport, { scrollHeight: 400, clientHeight: 100, scrollTop: 300 });
|
||||
|
||||
act(() => useSessionStore.getState().applyEvent({
|
||||
type: "activity_event",
|
||||
activity: { kind: "tool", toolCallId: "tool-2", toolName: "bash", status: "running" },
|
||||
}));
|
||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Hidden prompt" }));
|
||||
|
||||
expect(viewport.scrollTop).toBe(300);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user