Files
claw3d/tests/unit/taskBoardView.test.ts
T
Luke The Dev a997f13601 feat(kanban): Interactive Kanban board with real-time task tracking (#83)
* feat(kanban): add Kanban board with task-manager skill, modal UI, and desk clutter

Implement a full Kanban board system for tracking agent tasks:
- Add task-manager skill with shared JSON task store for persistence
- Render board as a floating modal over the live 3D office (not immersive)
- Auto-create tasks from actionable user messages with heuristic filtering
- Sync task status through OpenClaw agent lifecycle events
- Collapse task details panel by default, expand on card click
- Add dynamic desk clutter (papers, folders, etc.) reflecting active task count
- Exclude done tasks from desk clutter count
- Extract KANBAN_CLUTTER_OFFSET for easy positioning adjustment
- Add install flow with progress bar for the task-manager skill
- Include unit and e2e test coverage

Made-with: Cursor

* feat(kanban): production-harden task board with AI-free classification, resilient persistence, and modal UX

- Harden shared task store with atomic writes, payload size limits, and server-side enum validation
- Add client resilience: request timeouts (AbortController), exponential backoff retries, poll deduplication
- Implement optimistic UI with rollback on all card mutations (update, move, archive)
- Add modal accessibility: focus trap, Escape to close, aria-modal, keyboard card navigation
- Trust OpenClaw agent lifecycle phase=start as task classification signal instead of regex heuristics
- Keep regex heuristic only as lightweight filter for direct chat events (conversational noise)
- Expand verb recognition with typo tolerance and broader action vocabulary
- Create tasks from agent runs even when no chat event is received (external channel support)
- Merge dual header bars into single bar; reposition close button outside modal corner
- Exclude done tasks from desk clutter count; make clutter position configurable via KANBAN_CLUTTER_OFFSET
- Update default furniture layout to match user configuration
- Ensure kanban_board furniture persists in local storage across sessions
- Add comprehensive test coverage for store, API route, and controller logic

Made-with: Cursor

---------

Co-authored-by: iamlukethedev <lucas.guilherme@smartwayslfl.com>
2026-03-30 22:58:18 -05:00

138 lines
4.1 KiB
TypeScript

import { createElement } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { TaskBoardView } from "@/features/office/tasks/TaskBoardView";
import type { TaskBoardCard } from "@/features/office/tasks/types";
import type { AgentState } from "@/features/agents/state/store";
import type { CronJobSummary } from "@/lib/cron/types";
const createCard = (overrides: Partial<TaskBoardCard> = {}): TaskBoardCard => ({
id: "task-1",
title: "New task",
description: "",
status: "todo",
source: "claw3d_manual",
sourceEventId: null,
assignedAgentId: null,
createdAt: "2026-03-29T10:00:00.000Z",
updatedAt: "2026-03-29T10:00:00.000Z",
playbookJobId: null,
runId: null,
channel: null,
externalThreadId: null,
lastActivityAt: null,
notes: [],
isArchived: false,
isInferred: false,
...overrides,
});
const createAgent = (): AgentState => ({
agentId: "agent-1",
name: "Agent One",
sessionKey: "agent:agent-1:main",
status: "idle",
sessionCreated: true,
awaitingUserInput: false,
hasUnseenActivity: false,
outputLines: [],
lastResult: null,
lastDiff: null,
runId: null,
runStartedAt: null,
streamText: null,
thinkingTrace: null,
latestOverride: null,
latestOverrideKind: null,
lastAssistantMessageAt: null,
lastActivityAt: null,
latestPreview: null,
lastUserMessage: null,
draft: "",
sessionSettingsSynced: true,
historyLoadedAt: null,
historyFetchLimit: null,
historyFetchedCount: null,
historyMaybeTruncated: false,
toolCallingEnabled: true,
showThinkingTraces: true,
model: "openai/gpt-5",
thinkingLevel: "medium",
avatarSeed: "seed-1",
avatarUrl: null,
});
const createCronJob = (): CronJobSummary => ({
id: "job-1",
name: "Morning review",
agentId: "agent-1",
enabled: true,
updatedAtMs: Date.now(),
schedule: { kind: "every", everyMs: 60_000 },
sessionTarget: "isolated",
wakeMode: "now",
payload: { kind: "agentTurn", message: "Review new tasks." },
state: {},
});
describe("TaskBoardView", () => {
it("routes task edits through callbacks", () => {
const onCreateCard = vi.fn();
const onMoveCard = vi.fn();
const onSelectCard = vi.fn();
const onUpdateCard = vi.fn();
const onDeleteCard = vi.fn();
const onRefreshCronJobs = vi.fn();
const selectedCard = createCard();
render(
createElement(TaskBoardView, {
title: "Kanban",
subtitle: "Track tasks.",
agents: [createAgent()],
cardsByStatus: {
todo: [selectedCard],
in_progress: [],
blocked: [],
review: [],
done: [],
},
selectedCard,
activeRuns: [{ runId: "run-1", agentId: "agent-1", label: "Agent One" }],
cronJobs: [createCronJob()],
cronLoading: false,
cronError: null,
onCreateCard,
onMoveCard,
onSelectCard,
onUpdateCard,
onDeleteCard,
onRefreshCronJobs,
})
);
fireEvent.click(screen.getAllByRole("button", { name: /new task/i })[0]!);
fireEvent.click(screen.getByRole("button", { name: /refresh/i }));
fireEvent.click(screen.getAllByRole("button", { name: /new task/i })[1]!);
fireEvent.change(screen.getByLabelText("Title"), {
target: { value: "Create marketing website" },
});
fireEvent.change(screen.getByLabelText("Status"), {
target: { value: "in_progress" },
});
fireEvent.change(screen.getByLabelText("Assigned agent"), {
target: { value: "agent-1" },
});
fireEvent.click(screen.getByRole("button", { name: /delete task/i }));
expect(onCreateCard).toHaveBeenCalledTimes(1);
expect(onRefreshCronJobs).toHaveBeenCalledTimes(1);
expect(onSelectCard).toHaveBeenCalledWith("task-1");
expect(onUpdateCard).toHaveBeenCalledWith("task-1", { title: "Create marketing website" });
expect(onMoveCard).toHaveBeenCalledWith("task-1", "in_progress");
expect(onUpdateCard).toHaveBeenCalledWith("task-1", { assignedAgentId: "agent-1" });
expect(onDeleteCard).toHaveBeenCalledWith("task-1");
});
});