mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-02 04:39:29 -05:00
- Fix the clipboard functionality to work on non local hosts and https - Improvements in sockets on front-end and backend. Storing session in local browser storage for reconnect. Logic to prevent socket echos coausing rerender and performance issues. - Fixes and udpates to re-ordering logic in adding a new task, reordering items on the task table. - Allowing assignee to not be hardcoded enum. - Fix to Document Version Control (Improvements still needed in the Milkdown editor conversion to store in the docs. - Adding types to remove [any] typescript issues.
288 lines
8.8 KiB
TypeScript
288 lines
8.8 KiB
TypeScript
import { describe, test, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock data for testing
|
|
const mockTasks = [
|
|
{
|
|
id: 'task-1',
|
|
title: 'First task',
|
|
description: 'Description 1',
|
|
status: 'todo',
|
|
assignee: 'User',
|
|
task_order: 1,
|
|
feature: 'feature-1'
|
|
},
|
|
{
|
|
id: 'task-2',
|
|
title: 'Second task',
|
|
description: 'Description 2',
|
|
status: 'todo',
|
|
assignee: 'AI IDE Agent',
|
|
task_order: 2,
|
|
feature: 'feature-1'
|
|
},
|
|
{
|
|
id: 'task-3',
|
|
title: 'Third task',
|
|
description: 'Description 3',
|
|
status: 'todo',
|
|
assignee: 'Archon',
|
|
task_order: 3,
|
|
feature: 'feature-2'
|
|
},
|
|
{
|
|
id: 'task-4',
|
|
title: 'Fourth task',
|
|
description: 'Description 4',
|
|
status: 'doing',
|
|
assignee: 'User',
|
|
task_order: 1,
|
|
feature: 'feature-2'
|
|
}
|
|
]
|
|
|
|
describe('TasksTab - Task Reordering', () => {
|
|
let reorderTasks: any
|
|
let handleReorderTasks: any
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
})
|
|
|
|
describe('Sequential Ordering System', () => {
|
|
test('maintains sequential order (1, 2, 3, ...) after reordering', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Move task from index 0 to index 2
|
|
const reordered = moveTask(tasks, 0, 2)
|
|
|
|
// Check that task_order is sequential
|
|
expect(reordered[0].task_order).toBe(1)
|
|
expect(reordered[1].task_order).toBe(2)
|
|
expect(reordered[2].task_order).toBe(3)
|
|
})
|
|
|
|
test('updates task_order for all affected tasks', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Move last task to first position
|
|
const reordered = moveTask(tasks, 2, 0)
|
|
|
|
expect(reordered[0].id).toBe('task-3')
|
|
expect(reordered[0].task_order).toBe(1)
|
|
expect(reordered[1].id).toBe('task-1')
|
|
expect(reordered[1].task_order).toBe(2)
|
|
expect(reordered[2].id).toBe('task-2')
|
|
expect(reordered[2].task_order).toBe(3)
|
|
})
|
|
|
|
test('handles moving task within same status column', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Move middle task to end
|
|
const reordered = moveTask(tasks, 1, 2)
|
|
|
|
expect(reordered[0].id).toBe('task-1')
|
|
expect(reordered[1].id).toBe('task-3')
|
|
expect(reordered[2].id).toBe('task-2')
|
|
|
|
// All should have sequential ordering
|
|
reordered.forEach((task, index) => {
|
|
expect(task.task_order).toBe(index + 1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Batch Reorder Persistence', () => {
|
|
test('batches multiple reorder operations', () => {
|
|
const persistBatch = vi.fn()
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Simulate multiple rapid reorders
|
|
const reordered1 = moveTask(tasks, 0, 2)
|
|
const reordered2 = moveTask(reordered1, 1, 0)
|
|
|
|
// In actual implementation, these would be debounced
|
|
// and sent as a single batch update
|
|
expect(reordered2[0].task_order).toBe(1)
|
|
expect(reordered2[1].task_order).toBe(2)
|
|
expect(reordered2[2].task_order).toBe(3)
|
|
})
|
|
|
|
test('preserves lastUpdate timestamp for optimistic updates', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
const timestamp = Date.now()
|
|
|
|
const reordered = moveTask(tasks, 0, 2, timestamp)
|
|
|
|
// All reordered tasks should have the lastUpdate timestamp
|
|
reordered.forEach(task => {
|
|
expect(task.lastUpdate).toBe(timestamp)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Race Condition Prevention', () => {
|
|
test('ignores updates for deleted tasks', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
const deletedTaskId = 'task-2'
|
|
|
|
// Remove task-2 to simulate deletion
|
|
const afterDeletion = tasks.filter(t => t.id !== deletedTaskId)
|
|
|
|
// Try to reorder with deleted task - should handle gracefully
|
|
const reordered = afterDeletion.map((task, index) => ({
|
|
...task,
|
|
task_order: index + 1
|
|
}))
|
|
|
|
expect(reordered.length).toBe(2)
|
|
expect(reordered.find(t => t.id === deletedTaskId)).toBeUndefined()
|
|
})
|
|
|
|
test('handles concurrent updates with temporary task replacement', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
const tempTask = { ...tasks[0], title: 'Temporary update' }
|
|
|
|
// Replace task temporarily (optimistic update)
|
|
const withTemp = tasks.map(t =>
|
|
t.id === tempTask.id ? tempTask : t
|
|
)
|
|
|
|
expect(withTemp[0].title).toBe('Temporary update')
|
|
expect(withTemp[0].id).toBe(tasks[0].id)
|
|
})
|
|
|
|
test('maintains order consistency during concurrent operations', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Simulate two concurrent reorder operations
|
|
const reorder1 = moveTask([...tasks], 0, 2)
|
|
const reorder2 = moveTask([...tasks], 2, 1)
|
|
|
|
// Both should maintain sequential ordering
|
|
reorder1.forEach((task, index) => {
|
|
expect(task.task_order).toBe(index + 1)
|
|
})
|
|
|
|
reorder2.forEach((task, index) => {
|
|
expect(task.task_order).toBe(index + 1)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Cross-Status Reordering', () => {
|
|
test('handles moving task to different status column', () => {
|
|
const todoTasks = mockTasks.filter(t => t.status === 'todo')
|
|
const doingTasks = mockTasks.filter(t => t.status === 'doing')
|
|
|
|
// Move first todo task to doing column
|
|
const taskToMove = todoTasks[0]
|
|
const updatedTask = { ...taskToMove, status: 'doing' }
|
|
|
|
// Update todo column (remove task)
|
|
const newTodoTasks = todoTasks.slice(1).map((task, index) => ({
|
|
...task,
|
|
task_order: index + 1
|
|
}))
|
|
|
|
// Update doing column (add task at position)
|
|
const newDoingTasks = [
|
|
updatedTask,
|
|
...doingTasks
|
|
].map((task, index) => ({
|
|
...task,
|
|
task_order: index + 1
|
|
}))
|
|
|
|
// Verify sequential ordering in both columns
|
|
expect(newTodoTasks.every((t, i) => t.task_order === i + 1)).toBe(true)
|
|
expect(newDoingTasks.every((t, i) => t.task_order === i + 1)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
test('handles empty task list', () => {
|
|
const tasks: any[] = []
|
|
const reordered = moveTask(tasks, 0, 0)
|
|
|
|
expect(reordered).toEqual([])
|
|
})
|
|
|
|
test('handles single task', () => {
|
|
const tasks = [mockTasks[0]]
|
|
const reordered = moveTask(tasks, 0, 0)
|
|
|
|
expect(reordered[0].task_order).toBe(1)
|
|
expect(reordered.length).toBe(1)
|
|
})
|
|
|
|
test('handles invalid indices gracefully', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
|
|
// Try to move with out-of-bounds index
|
|
const reordered = moveTask(tasks, 10, 0)
|
|
|
|
// Should return tasks unchanged
|
|
expect(reordered).toEqual(tasks)
|
|
})
|
|
|
|
test('preserves task data during reorder', () => {
|
|
const tasks = [...mockTasks.filter(t => t.status === 'todo')]
|
|
const originalTask = { ...tasks[0] }
|
|
|
|
const reordered = moveTask(tasks, 0, 2)
|
|
const movedTask = reordered.find(t => t.id === originalTask.id)
|
|
|
|
// All properties except task_order should be preserved
|
|
expect(movedTask?.title).toBe(originalTask.title)
|
|
expect(movedTask?.description).toBe(originalTask.description)
|
|
expect(movedTask?.assignee).toBe(originalTask.assignee)
|
|
expect(movedTask?.feature).toBe(originalTask.feature)
|
|
})
|
|
})
|
|
|
|
describe('Flexible Assignee Support', () => {
|
|
test('supports any assignee name string', () => {
|
|
const customAssignees = [
|
|
'prp-executor',
|
|
'prp-validator',
|
|
'Custom Agent',
|
|
'test-agent-123'
|
|
]
|
|
|
|
customAssignees.forEach(assignee => {
|
|
const task = { ...mockTasks[0], assignee }
|
|
expect(task.assignee).toBe(assignee)
|
|
expect(typeof task.assignee).toBe('string')
|
|
})
|
|
})
|
|
|
|
test('handles empty assignee gracefully', () => {
|
|
const task = { ...mockTasks[0], assignee: '' }
|
|
expect(task.assignee).toBe('')
|
|
|
|
// Should default to 'AI IDE Agent' in UI
|
|
const displayAssignee = task.assignee || 'AI IDE Agent'
|
|
expect(displayAssignee).toBe('AI IDE Agent')
|
|
})
|
|
})
|
|
})
|
|
|
|
// Helper function to simulate task reordering
|
|
function moveTask(tasks: any[], fromIndex: number, toIndex: number, timestamp?: number): any[] {
|
|
if (fromIndex < 0 || fromIndex >= tasks.length ||
|
|
toIndex < 0 || toIndex >= tasks.length) {
|
|
return tasks
|
|
}
|
|
|
|
const result = [...tasks]
|
|
const [movedTask] = result.splice(fromIndex, 1)
|
|
result.splice(toIndex, 0, movedTask)
|
|
|
|
// Update task_order to be sequential
|
|
return result.map((task, index) => ({
|
|
...task,
|
|
task_order: index + 1,
|
|
...(timestamp ? { lastUpdate: timestamp } : {})
|
|
}))
|
|
} |