feat(frontend): streamline task creation and preview workflows

- remove prompt and preview generation from task creation

- create tasks as inactive and route directly to edit page

- add generated message history UX to edit task

- update entity/task views and related test coverage
This commit is contained in:
2026-03-27 02:23:56 -03:00
parent a83ea85857
commit f2a16b5cf6
10 changed files with 430 additions and 222 deletions

View File

@@ -229,6 +229,7 @@ TASK DETAILS
INSTRUCTIONS
- Write exactly one email message.
- The message must be written by Milton Fiscal (Director of Ceremonial Logistics) as the sender persona.
- Use an extremely formal corporate tone.
- Keep the content casual, mundane, and slightly nonsensical.
- Reflect the entity personality and task prompt faithfully.

View File

@@ -39,11 +39,6 @@ const mockEntity = {
describe('CreateTaskPage', () => {
beforeEach(() => {
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')
vi.mocked(tasksApi.buildTaskPreviewPrompt).mockImplementation(
(entity, task) =>
`PROMPT FOR ${entity.name}: ${task.name} | ${task.prompt} | ${task.scheduleCron} | ${task.emailLookback}`
)
vi.mocked(entitiesApi.getEntity).mockResolvedValue(mockEntity)
mockNavigate.mockClear()
})
@@ -54,25 +49,36 @@ describe('CreateTaskPage', () => {
await screen.findByRole('link', { name: /back to entity a/i })
expect(screen.getByLabelText(/task name/i)).toBeInTheDocument()
expect(screen.getByLabelText(/task prompt/i)).toBeInTheDocument()
expect(screen.queryByLabelText(/task prompt/i)).not.toBeInTheDocument()
expect(screen.getByLabelText(/^Email Period$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^Minute$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^Hour$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^Day of Month$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^Month$/i)).toBeInTheDocument()
expect(screen.getByLabelText(/^Day of Week$/i)).toBeInTheDocument()
expect(screen.queryByRole('button', { name: /generate test message/i })).not.toBeInTheDocument()
expect(screen.queryByText(/Final Prompt/i)).not.toBeInTheDocument()
})
it('should_generatePreviewAndCreateTask_when_formSubmitted', async () => {
vi.mocked(tasksApi.generateTaskPreview).mockResolvedValue('SUBJECT: Preview\nBODY:\nFormal nonsense')
it('should_createInactiveTaskAndNavigateToEditTask_when_formSubmitted', async () => {
vi.mocked(tasksApi.createTask).mockResolvedValue({
id: 'task-2',
entityId: 'entity-1',
name: 'Morning Blast',
prompt: 'Talk about coffee',
prompt: '',
scheduleCron: '0 8 * * 1-5',
emailLookback: 'last_week',
active: true,
active: false,
createdAt: '2026-03-26T10:00:00Z',
})
vi.mocked(tasksApi.inactivateTask).mockResolvedValue({
id: 'task-2',
entityId: 'entity-1',
name: 'Morning Blast',
prompt: '',
scheduleCron: '0 8 * * 1-5',
emailLookback: 'last_week',
active: false,
createdAt: '2026-03-26T10:00:00Z',
})
@@ -97,45 +103,10 @@ describe('CreateTaskPage', () => {
expect(screen.getByRole('button', { name: /Monthly/i })).toBeInTheDocument()
fireEvent.change(screen.getByLabelText(/task name/i), { target: { value: 'Morning Blast' } })
fireEvent.change(screen.getByLabelText(/task prompt/i), { target: { value: 'Talk about coffee' } })
fireEvent.click(screen.getByRole('button', { name: /Weekdays/i }))
fireEvent.change(screen.getByLabelText(/^Hour$/i), { target: { value: '8' } })
expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument()
expect(
screen.getByText('PROMPT FOR Entity A: Morning Blast | Talk about coffee | 0 8 * * 1-5 | last_week')
).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /generate test message/i }))
await waitFor(() => {
expect(tasksApi.generateTaskPreview).toHaveBeenCalled()
expect(vi.mocked(tasksApi.buildTaskPreviewPrompt)).toHaveBeenCalledWith(
mockEntity,
expect.objectContaining({
entityId: 'entity-1',
name: 'Morning Blast',
prompt: 'Talk about coffee',
scheduleCron: '0 8 * * 1-5',
emailLookback: 'last_week',
})
)
expect(vi.mocked(tasksApi.generateTaskPreview).mock.calls[0][0]).toEqual(
expect.objectContaining({
entity: mockEntity,
task: 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(() => {
@@ -144,32 +115,13 @@ describe('CreateTaskPage', () => {
expect.objectContaining({
entityId: 'entity-1',
name: 'Morning Blast',
prompt: 'Talk about coffee',
prompt: '',
scheduleCron: '0 8 * * 1-5',
emailLookback: 'last_week',
})
)
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1')
})
})
it('should_showReadablePreviewError_when_generationFails', async () => {
vi.mocked(tasksApi.generateTaskPreview).mockRejectedValue(
new Error('Unable to generate a test message from the local model. Connection refused')
)
render(<CreateTaskPage />, { wrapper })
await screen.findByRole('link', { name: /back to entity a/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.click(screen.getByRole('button', { name: /generate test message/i }))
await waitFor(() => {
expect(
screen.getByText(/Unable to generate a test message from the local model. Connection refused/i)
).toBeInTheDocument()
expect(tasksApi.inactivateTask).toHaveBeenCalledWith('task-2')
expect(mockNavigate).toHaveBeenCalledWith('/entities/entity-1/tasks/task-2')
})
})

View File

@@ -186,6 +186,59 @@ describe('EditTaskPage', () => {
})
})
it('should_renderGeneratedMessagePanels_when_pageLoads', async () => {
renderPage()
await screen.findByRole('link', { name: /back to entity a/i })
expect(screen.getByText(/Final Prompt/i)).toBeInTheDocument()
expect(screen.getByText(/^Generated Message$/i)).toBeInTheDocument()
expect(screen.getByText(/^Generated Message History$/i)).toBeInTheDocument()
expect(screen.getByText(/Generate a message and it will appear here./i)).toBeInTheDocument()
expect(screen.getByRole('list', { name: /generated message history/i })).toBeInTheDocument()
expect(screen.getByText(/No generated messages yet./i)).toBeInTheDocument()
})
it('should_showAndManageGeneratedMessageHistory_when_multipleMessagesGenerated', async () => {
vi.mocked(tasksApi.generateTaskPreview)
.mockResolvedValueOnce('SUBJECT: First\nBODY:\nFirst output')
.mockResolvedValueOnce('SUBJECT: Second\nBODY:\nSecond output')
renderPage()
await screen.findByRole('link', { name: /back to entity a/i })
const generateButton = screen.getByRole('button', { name: /generate test message/i })
fireEvent.click(generateButton)
await screen.findByText(/First output/i)
fireEvent.click(generateButton)
await waitFor(() => {
expect(screen.getByText(/Second output/i)).toBeInTheDocument()
})
const history = screen.getByRole('list', { name: /generated message history/i })
expect(history).toBeInTheDocument()
const firstMessageHistoryItem = screen.getByRole('button', { name: /^message #1$/i })
const secondMessageHistoryItem = screen.getByRole('button', { name: /^message #2$/i })
fireEvent.click(firstMessageHistoryItem)
expect(screen.getByText(/First output/i)).toBeInTheDocument()
fireEvent.click(
screen.getByRole('button', {
name: /delete message #1/i,
})
)
await waitFor(() => {
expect(firstMessageHistoryItem).not.toBeInTheDocument()
expect(secondMessageHistoryItem).toBeInTheDocument()
})
})
it('should_renderActivateButton_when_taskIsInactive', async () => {
renderPage({ task: { ...mockTask, active: false } })

View File

@@ -41,6 +41,37 @@ describe('EntitiesPage', () => {
fireEvent.click(addButton)
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument()
})
})
it('should_submitDefaultContextWindow_when_createEntitySubmitted', async () => {
vi.mocked(entitiesApi.getEntities).mockResolvedValue([])
vi.mocked(entitiesApi.createEntity).mockResolvedValue(mockEntity)
render(<EntitiesPage />, { wrapper })
fireEvent.click(screen.getByRole('button', { name: /add|create|new/i }))
await waitFor(() => {
expect(screen.getByRole('dialog', { name: /create entity/i })).toBeInTheDocument()
})
fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Test Entity' } })
fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'test@condado.com' } })
fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Tester' } })
fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Formal' } })
fireEvent.click(screen.getByRole('button', { name: /create/i }))
await waitFor(() => {
expect(entitiesApi.createEntity).toHaveBeenCalled()
expect(vi.mocked(entitiesApi.createEntity).mock.calls[0]?.[0]).toEqual({
name: 'Test Entity',
email: 'test@condado.com',
jobTitle: 'Tester',
personality: 'Formal',
contextWindowDays: 3,
})
})
})

View File

@@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react'
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'
@@ -20,6 +20,84 @@ const wrapper = ({ children }: { children: React.ReactNode }) => (
)
describe('EntityDetailPage', () => {
it('should_updateEntity_when_editSubmittedFromDetailPage', async () => {
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')
vi.mocked(entitiesApi.getEntity)
.mockResolvedValueOnce({
id: 'entity-1',
name: 'Entity A',
email: 'a@a.com',
jobTitle: 'Ops',
personality: 'Formal',
scheduleCron: '0 9 * * 1',
contextWindowDays: 3,
active: true,
createdAt: '',
})
.mockResolvedValueOnce({
id: 'entity-1',
name: 'Entity A Updated',
email: 'updated@a.com',
jobTitle: 'Operations Lead',
personality: 'Still formal',
scheduleCron: '0 9 * * 1',
contextWindowDays: 5,
active: true,
createdAt: '',
})
vi.mocked(entitiesApi.updateEntity).mockResolvedValue({
id: 'entity-1',
name: 'Entity A Updated',
email: 'updated@a.com',
jobTitle: 'Operations Lead',
personality: 'Still formal',
scheduleCron: '0 9 * * 1',
contextWindowDays: 5,
active: true,
createdAt: '',
})
vi.mocked(tasksApi.getTasksByEntity).mockResolvedValue([])
render(<EntityDetailPage />, { wrapper })
await waitFor(() => {
expect(screen.getByText(/Entity A/i)).toBeInTheDocument()
})
fireEvent.click(screen.getByRole('button', { name: /edit entity/i }))
await waitFor(() => {
expect(screen.getByRole('dialog', { name: /edit entity/i })).toBeInTheDocument()
expect(screen.getByLabelText(/entity name/i)).toHaveValue('Entity A')
expect(screen.getByLabelText(/sender email/i)).toHaveValue('a@a.com')
expect(screen.getByLabelText(/job title/i)).toHaveValue('Ops')
expect(screen.getByLabelText(/personality notes/i)).toHaveValue('Formal')
expect(screen.queryByLabelText(/default email context window/i)).not.toBeInTheDocument()
})
fireEvent.change(screen.getByLabelText(/entity name/i), { target: { value: 'Entity A Updated' } })
fireEvent.change(screen.getByLabelText(/sender email/i), { target: { value: 'updated@a.com' } })
fireEvent.change(screen.getByLabelText(/job title/i), { target: { value: 'Operations Lead' } })
fireEvent.change(screen.getByLabelText(/personality notes/i), { target: { value: 'Still formal' } })
fireEvent.click(screen.getByRole('button', { name: /^save$/i }))
await waitFor(() => {
expect(entitiesApi.updateEntity).toHaveBeenCalledWith('entity-1', {
name: 'Entity A Updated',
email: 'updated@a.com',
jobTitle: 'Operations Lead',
personality: 'Still formal',
contextWindowDays: 3,
})
expect(screen.queryByRole('dialog', { name: /edit entity/i })).not.toBeInTheDocument()
expect(screen.getByRole('heading', { name: /Entity A Updated/i })).toBeInTheDocument()
expect(screen.getByText(/Operations Lead - updated@a.com/i)).toBeInTheDocument()
})
})
it('should_renderEntityAndTasks_when_pageLoads', async () => {
vi.mocked(tasksApi.getEmailLookbackLabel).mockReturnValue('Last week')