diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.test.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.test.tsx
new file mode 100644
index 000000000..e061d1090
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.test.tsx
@@ -0,0 +1,220 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+// Track automation status filter state for assertions
+let automationStatusValue: string | null = null;
+const mockSetAutomationStatus = vi.fn((val: string | null) => {
+ automationStatusValue = val;
+});
+
+// Mock nuqs
+vi.mock('nuqs', () => ({
+ useQueryState: (key: string) => {
+ if (key === 'automationStatus') return [automationStatusValue, mockSetAutomationStatus];
+ return [null, vi.fn()];
+ },
+}));
+
+// Mock next/navigation
+vi.mock('next/navigation', () => ({
+ useParams: () => ({ orgId: 'org_123' }),
+}));
+
+// Mock child components
+vi.mock('./ModernTaskList', () => ({
+ ModernTaskList: ({ tasks }: { tasks: { id: string }[] }) => (
+
+ {tasks.map((t) => (
+
+ ))}
+
+ ),
+}));
+
+vi.mock('./TasksByCategory', () => ({
+ TasksByCategory: ({ tasks }: { tasks: { id: string }[] }) => (
+
+ {tasks.map((t) => (
+
+ ))}
+
+ ),
+}));
+
+// Mock lucide-react icons
+vi.mock('lucide-react', () => ({
+ Check: () => ,
+ Circle: () => ,
+ FolderTree: () => ,
+ List: () => ,
+ Search: () => ,
+ XCircle: () => ,
+}));
+
+// Mock design-system components
+vi.mock('@trycompai/design-system', () => ({
+ Avatar: ({ children }: { children: React.ReactNode }) => {children}
,
+ AvatarFallback: ({ children }: { children: React.ReactNode }) => {children},
+ AvatarImage: () =>
,
+ HStack: ({ children }: { children: React.ReactNode }) => {children}
,
+ InputGroup: ({ children }: { children: React.ReactNode }) => {children}
,
+ InputGroupAddon: ({ children }: { children: React.ReactNode }) => {children}
,
+ InputGroupInput: (props: Record) => ,
+ Select: ({
+ children,
+ value,
+ onValueChange,
+ }: {
+ children: React.ReactNode;
+ value: string;
+ onValueChange: (v: string) => void;
+ }) => (
+
+ {children}
+
+
+ ),
+ SelectContent: ({ children }: { children: React.ReactNode }) => {children}
,
+ SelectItem: ({
+ children,
+ value,
+ }: {
+ children: React.ReactNode;
+ value: string;
+ }) => (
+
+ ),
+ SelectTrigger: ({
+ children,
+ }: {
+ children: React.ReactNode;
+ size?: string;
+ disabled?: boolean;
+ }) => {children}
,
+ SelectValue: ({
+ children,
+ }: {
+ children: React.ReactNode;
+ placeholder?: string;
+ }) => {children}
,
+ Separator: () =>
,
+ Stack: ({ children }: { children: React.ReactNode }) => {children}
,
+ Tabs: ({
+ children,
+ }: {
+ children: React.ReactNode;
+ value?: string;
+ onValueChange?: (v: string) => void;
+ }) => {children}
,
+ TabsContent: ({ children }: { children: React.ReactNode; value?: string }) => (
+ {children}
+ ),
+ TabsList: ({ children }: { children: React.ReactNode; variant?: string }) => (
+ {children}
+ ),
+ TabsTrigger: ({ children }: { children: React.ReactNode; value?: string }) => (
+ {children}
+ ),
+ Text: ({ children }: { children: React.ReactNode }) => {children},
+}));
+
+import { TaskList } from './TaskList';
+
+const baseMockTask = {
+ description: 'Test',
+ status: 'todo' as const,
+ frequency: null,
+ department: null,
+ assigneeId: null,
+ organizationId: 'org_123',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ order: 0,
+ taskTemplateId: null,
+ reviewDate: null,
+ approvalStatus: null,
+ approverId: null,
+ approvedAt: null,
+ approvalComment: null,
+ controls: [] as { id: string; name: string }[],
+};
+
+const automatedTask = {
+ ...baseMockTask,
+ id: 'task_auto_1',
+ title: 'Automated Task',
+ automationStatus: 'AUTOMATED' as const,
+};
+
+const manualTask = {
+ ...baseMockTask,
+ id: 'task_manual_1',
+ title: 'Manual Task',
+ automationStatus: 'MANUAL' as const,
+};
+
+const defaultProps = {
+ tasks: [automatedTask, manualTask],
+ members: [],
+ frameworkInstances: [],
+ activeTab: 'list' as const,
+ evidenceApprovalEnabled: false,
+};
+
+describe('TaskList automation status filter', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ automationStatusValue = null;
+ });
+
+ it('renders the automation status filter dropdown', () => {
+ render();
+ expect(screen.getAllByText('All types').length).toBeGreaterThan(0);
+ });
+
+ it('shows all tasks when no automation status filter is active', () => {
+ render();
+ expect(screen.getByTestId('task-task_auto_1')).toBeInTheDocument();
+ expect(screen.getByTestId('task-task_manual_1')).toBeInTheDocument();
+ });
+
+ it('shows only automated tasks when AUTOMATED filter is active', () => {
+ automationStatusValue = 'AUTOMATED';
+ render();
+ expect(screen.getByTestId('task-task_auto_1')).toBeInTheDocument();
+ expect(screen.queryByTestId('task-task_manual_1')).not.toBeInTheDocument();
+ });
+
+ it('shows only manual tasks when MANUAL filter is active', () => {
+ automationStatusValue = 'MANUAL';
+ render();
+ expect(screen.queryByTestId('task-task_auto_1')).not.toBeInTheDocument();
+ expect(screen.getByTestId('task-task_manual_1')).toBeInTheDocument();
+ });
+
+ it('displays result count when automation status filter is active', () => {
+ automationStatusValue = 'AUTOMATED';
+ render();
+ expect(screen.getByText('1 result')).toBeInTheDocument();
+ });
+
+ it('renders Automated and Manual options in the dropdown', () => {
+ render();
+ expect(screen.getAllByTestId('select-item-AUTOMATED')).toHaveLength(1);
+ expect(screen.getAllByTestId('select-item-MANUAL')).toHaveLength(1);
+ });
+
+ it('renders All types text in the dropdown', () => {
+ render();
+ expect(screen.getAllByText('All types').length).toBeGreaterThan(0);
+ });
+});
diff --git a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx
index a19a9c558..c49e6556c 100644
--- a/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx
+++ b/apps/app/src/app/(app)/[orgId]/tasks/components/TaskList.tsx
@@ -79,6 +79,8 @@ export function TaskList({
const [statusFilter, setStatusFilter] = useQueryState('status');
const [assigneeFilter, setAssigneeFilter] = useQueryState('assignee');
const [frameworkFilter, setFrameworkFilter] = useQueryState('framework');
+ const [automationStatusFilter, setAutomationStatusFilter] =
+ useQueryState('automationStatus');
const [currentTab, setCurrentTab] = useState<'categories' | 'list'>(activeTab);
// Sync activeTab prop with state when it changes
@@ -154,7 +156,16 @@ export function TaskList({
return task.controls.some((c) => fwControlIds.has(c.id));
})();
- return matchesSearch && matchesStatus && matchesAssignee && matchesFramework;
+ const matchesAutomationStatus =
+ !automationStatusFilter || task.automationStatus === automationStatusFilter;
+
+ return (
+ matchesSearch &&
+ matchesStatus &&
+ matchesAssignee &&
+ matchesFramework &&
+ matchesAutomationStatus
+ );
});
// Calculate overall stats from all tasks (not filtered)
@@ -719,9 +730,37 @@ export function TaskList({
))}
+
+
{/* Result Count */}
- {(searchQuery || statusFilter || assigneeFilter || frameworkFilter) && (
+ {(searchQuery || statusFilter || assigneeFilter || frameworkFilter || automationStatusFilter) && (
{filteredTasks.length} {filteredTasks.length === 1 ? 'result' : 'results'}