diff --git a/frontend/src/__tests__/pages/DashboardPage.test.tsx b/frontend/src/__tests__/pages/DashboardPage.test.tsx
index 85aa259..e925511 100644
--- a/frontend/src/__tests__/pages/DashboardPage.test.tsx
+++ b/frontend/src/__tests__/pages/DashboardPage.test.tsx
@@ -44,4 +44,29 @@ describe('DashboardPage', () => {
expect(screen.getByText(/Memo/i)).toBeInTheDocument()
})
})
+
+ it('should_renderScheduledTasks_when_entitiesHaveSchedule', async () => {
+ vi.mocked(entitiesApi.getEntities).mockResolvedValue([
+ {
+ id: '1',
+ name: 'Entity A',
+ email: 'a@a.com',
+ jobTitle: 'Ops',
+ personality: '',
+ scheduleCron: '0 9 * * 1',
+ contextWindowDays: 3,
+ active: true,
+ createdAt: '',
+ },
+ ])
+ vi.mocked(logsApi.getLogs).mockResolvedValue([])
+
+ render(, { wrapper })
+
+ await waitFor(() => {
+ expect(screen.getByText(/Scheduled Tasks/i)).toBeInTheDocument()
+ expect(screen.getByText(/Entity A/i)).toBeInTheDocument()
+ expect(screen.getByText(/0 9 \* \* 1/i)).toBeInTheDocument()
+ })
+ })
})
\ No newline at end of file
diff --git a/frontend/src/__tests__/pages/EntitiesPage.test.tsx b/frontend/src/__tests__/pages/EntitiesPage.test.tsx
index 21bbe3d..1321703 100644
--- a/frontend/src/__tests__/pages/EntitiesPage.test.tsx
+++ b/frontend/src/__tests__/pages/EntitiesPage.test.tsx
@@ -57,4 +57,13 @@ describe('EntitiesPage', () => {
expect(entitiesApi.deleteEntity).toHaveBeenCalledWith('entity-1')
})
})
+
+ it('should_renderDetailLink_when_entitiesLoaded', async () => {
+ vi.mocked(entitiesApi.getEntities).mockResolvedValue([mockEntity])
+ render(, { wrapper })
+
+ await waitFor(() => {
+ expect(screen.getByRole('link', { name: /open details/i })).toHaveAttribute('href', '/entities/entity-1')
+ })
+ })
})
diff --git a/frontend/src/__tests__/pages/EntityDetailPage.test.tsx b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx
new file mode 100644
index 0000000..6a0b0ec
--- /dev/null
+++ b/frontend/src/__tests__/pages/EntityDetailPage.test.tsx
@@ -0,0 +1,118 @@
+import { render, screen, waitFor, fireEvent } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { MemoryRouter, Route, Routes } from 'react-router-dom'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import EntityDetailPage from '@/pages/EntityDetailPage'
+import * as entitiesApi from '@/api/entitiesApi'
+import * as tasksApi from '@/api/tasksApi'
+
+vi.mock('@/api/entitiesApi')
+vi.mock('@/api/tasksApi')
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+
+
+
+
+
+)
+
+describe('EntityDetailPage', () => {
+ it('should_renderEntityAndTasks_when_pageLoads', async () => {
+ vi.mocked(entitiesApi.getEntity).mockResolvedValue({
+ id: 'entity-1',
+ name: 'Entity A',
+ email: 'a@a.com',
+ jobTitle: 'Ops',
+ personality: 'Formal',
+ scheduleCron: '0 9 * * 1',
+ contextWindowDays: 3,
+ active: true,
+ createdAt: '',
+ })
+ vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([
+ {
+ id: 'task-1',
+ entityId: 'entity-1',
+ name: 'Weekly Check-in',
+ prompt: 'Summarize jokes',
+ scheduleCron: '0 9 * * 1',
+ emailLookback: 'last_week',
+ createdAt: '2026-03-26T10:00:00Z',
+ },
+ ])
+
+ render(, { wrapper })
+
+ await waitFor(() => {
+ expect(screen.getByText(/Entity A/i)).toBeInTheDocument()
+ expect(screen.getByText(/Weekly Check-in/i)).toBeInTheDocument()
+ expect(screen.getByText(/Last week/i)).toBeInTheDocument()
+ })
+ })
+
+ it('should_generatePreviewAndCreateTask_when_formSubmitted', async () => {
+ vi.mocked(entitiesApi.getEntity).mockResolvedValue({
+ id: 'entity-1',
+ name: 'Entity A',
+ email: 'a@a.com',
+ jobTitle: 'Ops',
+ personality: 'Formal',
+ scheduleCron: '0 9 * * 1',
+ contextWindowDays: 3,
+ active: true,
+ createdAt: '',
+ })
+ vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([])
+ vi.mocked(tasksApi.generateTaskPreview).mockResolvedValue('SUBJECT: Preview\nBODY:\nFormal nonsense')
+ vi.mocked(tasksApi.createTask).mockResolvedValue({
+ id: 'task-2',
+ entityId: 'entity-1',
+ name: 'Morning Blast',
+ prompt: 'Talk about coffee',
+ scheduleCron: '0 8 * * 1-5',
+ emailLookback: 'last_week',
+ createdAt: '2026-03-26T10:00:00Z',
+ })
+
+ render(, { wrapper })
+
+ await screen.findByRole('button', { name: /new task/i })
+ fireEvent.click(screen.getByRole('button', { name: /new task/i }))
+
+ fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } })
+ fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } })
+ fireEvent.change(screen.getByLabelText(/task schedule/i), { target: { value: '0 8 * * 1-5' } })
+
+ fireEvent.click(screen.getByRole('button', { name: /generate test message/i }))
+
+ await waitFor(() => {
+ expect(tasksApi.generateTaskPreview).toHaveBeenCalledWith(
+ expect.objectContaining({
+ entityId: 'entity-1',
+ name: 'Morning Blast',
+ prompt: 'Talk about coffee',
+ scheduleCron: '0 8 * * 1-5',
+ emailLookback: 'last_week',
+ })
+ )
+ expect(screen.getByText(/Formal nonsense/i)).toBeInTheDocument()
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: /create task/i }))
+
+ await waitFor(() => {
+ expect(tasksApi.createTask).toHaveBeenCalledWith(
+ expect.objectContaining({
+ entityId: 'entity-1',
+ name: 'Morning Blast',
+ prompt: 'Talk about coffee',
+ scheduleCron: '0 8 * * 1-5',
+ emailLookback: 'last_week',
+ })
+ )
+ })
+ })
+})