feat(#37-41): Add domains, ideas, relationships, agents, widgets schema

Schema additions for issues #37-41:

New models:
- Domain (#37): Life domains (work, marriage, homelab, etc.)
- Idea (#38): Brain dumps with pgvector embeddings
- Relationship (#39): Generic entity linking (blocks, depends_on)
- Agent (#40): ClawdBot agent tracking with metrics
- AgentSession (#40): Conversation session tracking
- WidgetDefinition (#41): HUD widget registry
- UserLayout (#41): Per-user dashboard configuration

Updated models:
- Task, Event, Project: Added domainId foreign key
- User, Workspace: Added new relations

New enums:
- IdeaStatus: CAPTURED, PROCESSING, ACTIONABLE, ARCHIVED, DISCARDED
- RelationshipType: BLOCKS, BLOCKED_BY, DEPENDS_ON, etc.
- AgentStatus: IDLE, WORKING, WAITING, ERROR, TERMINATED
- EntityType: Added IDEA, DOMAIN

Migration: 20260129182803_add_domains_ideas_agents_widgets
This commit is contained in:
Jason Woltje
2026-01-29 12:29:21 -06:00
parent a220c2dc0a
commit 973502f26e
308 changed files with 18374 additions and 113 deletions

View File

@@ -0,0 +1,227 @@
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { TaskItem } from "./TaskItem";
import { TaskStatus, TaskPriority, type Task } from "@mosaic/shared";
describe("TaskItem", () => {
const baseTask: Task = {
id: "task-1",
title: "Test task",
description: "Task description",
status: TaskStatus.IN_PROGRESS,
priority: TaskPriority.MEDIUM,
dueDate: new Date("2026-01-29"),
creatorId: "user-1",
assigneeId: "user-1",
workspaceId: "workspace-1",
projectId: null,
parentId: null,
sortOrder: 0,
metadata: {},
completedAt: null,
createdAt: new Date("2026-01-28"),
updatedAt: new Date("2026-01-28"),
};
it("should render task title", () => {
render(<TaskItem task={baseTask} />);
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should render task description when present", () => {
render(<TaskItem task={baseTask} />);
expect(screen.getByText("Task description")).toBeInTheDocument();
});
it("should show status indicator for active task", () => {
render(<TaskItem task={{ ...baseTask, status: TaskStatus.IN_PROGRESS }} />);
expect(screen.getByText("🟢")).toBeInTheDocument();
});
it("should show status indicator for not started task", () => {
render(<TaskItem task={{ ...baseTask, status: TaskStatus.NOT_STARTED }} />);
expect(screen.getByText("⚪")).toBeInTheDocument();
});
it("should show status indicator for paused task", () => {
render(<TaskItem task={{ ...baseTask, status: TaskStatus.PAUSED }} />);
expect(screen.getByText("⏸️")).toBeInTheDocument();
});
it("should display priority badge", () => {
render(<TaskItem task={{ ...baseTask, priority: TaskPriority.HIGH }} />);
expect(screen.getByText("High priority")).toBeInTheDocument();
});
it("should not use demanding language", () => {
const { container } = render(<TaskItem task={baseTask} />);
const text = container.textContent;
expect(text).not.toMatch(/overdue/i);
expect(text).not.toMatch(/urgent/i);
expect(text).not.toMatch(/must/i);
expect(text).not.toMatch(/critical/i);
});
it("should show 'Target passed' for past due dates", () => {
const pastTask = {
...baseTask,
dueDate: new Date("2026-01-27"), // Past date
};
render(<TaskItem task={pastTask} />);
expect(screen.getByText(/target passed/i)).toBeInTheDocument();
});
it("should show 'Approaching target' for near due dates", () => {
const soonTask = {
...baseTask,
dueDate: new Date(Date.now() + 12 * 60 * 60 * 1000), // 12 hours from now
};
render(<TaskItem task={soonTask} />);
expect(screen.getByText(/approaching target/i)).toBeInTheDocument();
});
describe("error states", () => {
it("should handle task with missing title", () => {
const taskWithoutTitle = {
...baseTask,
title: "",
};
const { container } = render(<TaskItem task={taskWithoutTitle} />);
// Should render without crashing, even with empty title
expect(container.querySelector(".bg-white")).toBeInTheDocument();
});
it("should handle task with missing description", () => {
const taskWithoutDescription = {
...baseTask,
description: null,
};
render(<TaskItem task={taskWithoutDescription} />);
expect(screen.getByText("Test task")).toBeInTheDocument();
// Description paragraph should not be rendered when null
expect(screen.queryByText("Task description")).not.toBeInTheDocument();
});
it("should handle task with invalid status", () => {
const taskWithInvalidStatus = {
...baseTask,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
status: "invalid-status" as any,
};
const { container } = render(<TaskItem task={taskWithInvalidStatus} />);
// Should render without crashing even with invalid status
expect(container.querySelector(".bg-white")).toBeInTheDocument();
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should handle task with invalid priority", () => {
const taskWithInvalidPriority = {
...baseTask,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
priority: "invalid-priority" as any,
};
const { container } = render(<TaskItem task={taskWithInvalidPriority} />);
// Should render without crashing even with invalid priority
expect(container.querySelector(".bg-white")).toBeInTheDocument();
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should handle task with missing dueDate", () => {
const taskWithoutDueDate = {
...baseTask,
dueDate: null,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render(<TaskItem task={taskWithoutDueDate as any} />);
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should handle task with invalid dueDate", () => {
const taskWithInvalidDate = {
...baseTask,
dueDate: new Date("invalid-date"),
};
const { container } = render(<TaskItem task={taskWithInvalidDate} />);
expect(container.querySelector(".bg-white")).toBeInTheDocument();
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should handle task with very long title", () => {
const longTitle = "A".repeat(500);
const taskWithLongTitle = {
...baseTask,
title: longTitle,
};
render(<TaskItem task={taskWithLongTitle} />);
expect(screen.getByText(longTitle)).toBeInTheDocument();
});
it("should handle task with special characters in title", () => {
const taskWithSpecialChars = {
...baseTask,
title: '<img src="x" onerror="alert(1)">',
};
const { container } = render(<TaskItem task={taskWithSpecialChars} />);
// Should render escaped HTML entities, not execute
// React escapes to &lt;img... &gt; which is safe
expect(container.innerHTML).toContain("&lt;img");
expect(container.innerHTML).not.toContain("<img src=");
// Text should be displayed as-is
expect(screen.getByText(/<img src="x" onerror="alert\(1\)">/)).toBeInTheDocument();
});
it("should handle task with HTML in description", () => {
const taskWithHtmlDesc = {
...baseTask,
description: '<b>Bold text</b><script>alert("xss")</script>',
};
const { container } = render(<TaskItem task={taskWithHtmlDesc} />);
// Should render as text, not HTML - React escapes by default
expect(container.innerHTML).not.toContain("<script>");
// Text should be displayed as-is
expect(screen.getByText(/Bold text/)).toBeInTheDocument();
});
it("should handle task with missing required IDs", () => {
const taskWithMissingIds = {
...baseTask,
id: "",
workspaceId: "",
};
const { container } = render(<TaskItem task={taskWithMissingIds} />);
expect(container.querySelector(".bg-white")).toBeInTheDocument();
expect(screen.getByText("Test task")).toBeInTheDocument();
});
it("should handle task with extremely old due date", () => {
const veryOldTask = {
...baseTask,
dueDate: new Date("1970-01-01"),
};
render(<TaskItem task={veryOldTask} />);
expect(screen.getByText(/target passed/i)).toBeInTheDocument();
});
it("should handle task with far future due date", () => {
const farFutureTask = {
...baseTask,
dueDate: new Date("2099-12-31"),
};
const { container } = render(<TaskItem task={farFutureTask} />);
expect(container.querySelector(".bg-white")).toBeInTheDocument();
expect(screen.getByText("Test task")).toBeInTheDocument();
});
});
});