fix(frontend): deduplicate live workflow activity

This commit is contained in:
User
2026-07-15 11:55:14 +02:00
parent e7a8bc29b0
commit 279b79516c
8 changed files with 169 additions and 183 deletions
+18 -24
View File
@@ -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);
});