Files
OpenHands/frontend/__tests__/hooks/use-task-list.test.ts

280 lines
8.2 KiB
TypeScript

import { describe, expect, it, beforeEach } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useTaskList } from "#/hooks/use-task-list";
import { useEventStore } from "#/stores/use-event-store";
import type { OHEvent } from "#/stores/use-event-store";
import type { TaskTrackingObservation } from "#/types/core/observations";
function createV0TaskTrackingObservation(
id: number,
command: string,
taskList: TaskTrackingObservation["extras"]["task_list"],
): TaskTrackingObservation {
return {
id,
source: "agent",
observation: "task_tracking",
message: "Task tracking update",
timestamp: `2025-07-01T00:00:0${id}Z`,
cause: 0,
content: "",
extras: {
command,
task_list: taskList,
},
};
}
function createV1TaskTrackerObservation(
id: string,
command: string,
taskList: Array<{
title: string;
notes: string;
status: "todo" | "in_progress" | "done";
}>,
): OHEvent {
return {
id,
timestamp: `2025-07-01T00:00:0${id}Z`,
source: "environment",
tool_name: "task_tracker",
tool_call_id: `call_${id}`,
action_id: `action_${id}`,
observation: {
kind: "TaskTrackerObservation",
content: "Task list updated",
command,
task_list: taskList,
},
} as unknown as OHEvent;
}
beforeEach(() => {
useEventStore.setState({
events: [],
eventIds: new Set(),
uiEvents: [],
});
});
describe("useTaskList", () => {
it("returns empty taskList and hasTaskList=false when no events exist", () => {
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
it("returns empty taskList when no task tracking observations exist", () => {
useEventStore.setState({
events: [
{
id: 1,
source: "user",
action: "message",
args: { content: "Hello", image_urls: [], file_urls: [] },
message: "Hello",
timestamp: "2025-07-01T00:00:01Z",
},
],
eventIds: new Set([1]),
uiEvents: [],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
describe("v0 events", () => {
it('returns the task list from a TaskTrackingObservation with command="plan"', () => {
const tasks = [
{ id: "1", title: "First task", status: "todo" as const },
{ id: "2", title: "Second task", status: "in_progress" as const },
];
const event = createV0TaskTrackingObservation(1, "plan", tasks);
useEventStore.setState({
events: [event],
eventIds: new Set([1]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual(tasks);
expect(result.current.hasTaskList).toBe(true);
});
it('ignores TaskTrackingObservation events with command !== "plan"', () => {
const tasks = [{ id: "1", title: "First task", status: "todo" as const }];
const event = createV0TaskTrackingObservation(1, "update", tasks);
useEventStore.setState({
events: [event],
eventIds: new Set([1]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
it("returns the latest task list when multiple plan events exist", () => {
const earlyTasks = [
{ id: "1", title: "First task", status: "todo" as const },
];
const lateTasks = [
{ id: "1", title: "First task", status: "done" as const },
{ id: "2", title: "New task", status: "in_progress" as const },
];
const event1 = createV0TaskTrackingObservation(1, "plan", earlyTasks);
const event2 = createV0TaskTrackingObservation(2, "plan", lateTasks);
useEventStore.setState({
events: [event1, event2],
eventIds: new Set([1, 2]),
uiEvents: [event1, event2],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual(lateTasks);
expect(result.current.hasTaskList).toBe(true);
});
it("updates when new events are added to the store", () => {
const { result } = renderHook(() => useTaskList());
expect(result.current.hasTaskList).toBe(false);
const tasks = [{ id: "1", title: "New task", status: "todo" as const }];
const event = createV0TaskTrackingObservation(1, "plan", tasks);
act(() => {
useEventStore.setState({
events: [event],
eventIds: new Set([1]),
uiEvents: [event],
});
});
expect(result.current.taskList).toEqual(tasks);
expect(result.current.hasTaskList).toBe(true);
});
it("returns hasTaskList=false when the latest plan has an empty task list", () => {
const event = createV0TaskTrackingObservation(1, "plan", []);
useEventStore.setState({
events: [event],
eventIds: new Set([1]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
});
describe("v1 events", () => {
it('returns the task list from a v1 TaskTrackerObservation with command="plan"', () => {
const tasks = [
{ title: "First task", notes: "", status: "todo" as const },
{
title: "Second task",
notes: "some note",
status: "in_progress" as const,
},
];
const event = createV1TaskTrackerObservation("1", "plan", tasks);
useEventStore.setState({
events: [event],
eventIds: new Set(["1"]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([
{ id: "1", title: "First task", notes: undefined, status: "todo" },
{
id: "2",
title: "Second task",
notes: "some note",
status: "in_progress",
},
]);
expect(result.current.hasTaskList).toBe(true);
});
it('ignores v1 TaskTrackerObservation with command !== "plan"', () => {
const tasks = [
{ title: "First task", notes: "", status: "todo" as const },
];
const event = createV1TaskTrackerObservation("1", "view", tasks);
useEventStore.setState({
events: [event],
eventIds: new Set(["1"]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
it("returns the latest v1 task list when multiple plan events exist", () => {
const earlyTasks = [
{ title: "First task", notes: "", status: "todo" as const },
];
const lateTasks = [
{ title: "First task", notes: "", status: "done" as const },
{ title: "New task", notes: "wip", status: "in_progress" as const },
];
const event1 = createV1TaskTrackerObservation("1", "plan", earlyTasks);
const event2 = createV1TaskTrackerObservation("2", "plan", lateTasks);
useEventStore.setState({
events: [event1, event2],
eventIds: new Set(["1", "2"]),
uiEvents: [event1, event2],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([
{ id: "1", title: "First task", notes: undefined, status: "done" },
{ id: "2", title: "New task", notes: "wip", status: "in_progress" },
]);
expect(result.current.hasTaskList).toBe(true);
});
it("returns hasTaskList=false when the latest v1 plan has an empty task list", () => {
const event = createV1TaskTrackerObservation("1", "plan", []);
useEventStore.setState({
events: [event],
eventIds: new Set(["1"]),
uiEvents: [event],
});
const { result } = renderHook(() => useTaskList());
expect(result.current.taskList).toEqual([]);
expect(result.current.hasTaskList).toBe(false);
});
});
});