feat(llm-gateway): implement LLM Gateway MCP Server (#56) #71

Closed
cardosofelipe wants to merge 103 commits from feature/56-llm-gateway-mcp-server into dev
8 changed files with 4881 additions and 165 deletions
Showing only changes of commit 731a188a76 - Show all commits

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,14 @@
* Provides data for the projects list page with filtering,
* sorting, and pagination.
*
* Uses mock data until backend endpoints are available.
* Uses SDK to call API (intercepted by MSW in demo mode).
*
* @see Issue #54
*/
import { useQuery } from '@tanstack/react-query';
import { listProjects as listProjectsApi } from '@/lib/api/generated';
import type { ProjectStatus as ApiProjectStatus, ProjectResponse } from '@/lib/api/generated';
import type { ProjectStatus } from '@/components/projects/types';
// ============================================================================
@@ -19,6 +21,7 @@ import type { ProjectStatus } from '@/components/projects/types';
export interface ProjectListItem {
id: string;
name: string;
slug: string;
description?: string;
status: ProjectStatus;
complexity: 'low' | 'medium' | 'high';
@@ -33,6 +36,7 @@ export interface ProjectListItem {
name: string;
};
tags?: string[];
autonomyLevel: string;
}
export interface ProjectsListParams {
@@ -56,134 +60,64 @@ export interface ProjectsListResponse {
}
// ============================================================================
// Mock Data
// Helpers
// ============================================================================
const mockProjects: ProjectListItem[] = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
description:
'Complete redesign of the e-commerce platform with modern UI/UX and improved checkout flow',
status: 'active',
complexity: 'high',
progress: 67,
openIssues: 12,
activeAgents: 4,
currentSprint: 'Sprint 3',
lastActivity: '2 minutes ago',
createdAt: '2025-11-15T10:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['e-commerce', 'frontend', 'ux'],
},
{
id: 'proj-002',
name: 'Mobile Banking App',
description:
'Native mobile app for banking services with biometric authentication and real-time notifications',
status: 'active',
complexity: 'high',
progress: 45,
openIssues: 8,
activeAgents: 5,
currentSprint: 'Sprint 2',
lastActivity: '15 minutes ago',
createdAt: '2025-11-20T09:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['mobile', 'fintech', 'security'],
},
{
id: 'proj-003',
name: 'Internal HR Portal',
description:
'Employee self-service portal for HR operations including leave requests and performance reviews',
status: 'paused',
complexity: 'medium',
progress: 23,
openIssues: 5,
activeAgents: 0,
currentSprint: 'Sprint 1',
lastActivity: '2 days ago',
createdAt: '2025-10-01T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['internal', 'hr', 'portal'],
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
description:
'Migrate legacy API gateway to cloud-native architecture with improved rate limiting and caching',
status: 'active',
complexity: 'high',
progress: 82,
openIssues: 3,
activeAgents: 2,
currentSprint: 'Sprint 4',
lastActivity: '1 hour ago',
createdAt: '2025-12-01T11:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['api', 'cloud', 'infrastructure'],
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
description:
'Real-time analytics dashboard for customer behavior insights with ML-powered predictions',
status: 'completed',
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
createdAt: '2025-09-01T10:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['analytics', 'ml', 'dashboard'],
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments and rollback capabilities',
status: 'active',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
currentSprint: 'Sprint 1',
lastActivity: '30 minutes ago',
createdAt: '2025-12-10T14:00:00Z',
owner: { id: 'user-001', name: 'Felipe Cardoso' },
tags: ['devops', 'automation', 'ci-cd'],
},
{
id: 'proj-007',
name: 'Inventory Management System',
description: 'Warehouse inventory tracking with barcode scanning and automated reordering',
status: 'archived',
complexity: 'low',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
createdAt: '2025-06-15T08:00:00Z',
owner: { id: 'user-002', name: 'Maria Santos' },
tags: ['inventory', 'warehouse', 'logistics'],
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
description: 'AI-powered chatbot for 24/7 customer support with sentiment analysis',
status: 'active',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
currentSprint: 'Sprint 2',
lastActivity: '45 minutes ago',
createdAt: '2025-12-05T09:00:00Z',
owner: { id: 'user-003', name: 'Alex Johnson' },
tags: ['ai', 'chatbot', 'support'],
},
];
/**
* Maps API ProjectResponse to our ProjectListItem format
*/
function mapProjectResponse(project: ProjectResponse & Record<string, unknown>): ProjectListItem {
// Map complexity from mock data format to UI format
const rawComplexity = project.complexity as string;
let complexity: 'low' | 'medium' | 'high' = 'medium';
if (rawComplexity === 'script' || rawComplexity === 'simple' || rawComplexity === 'low') {
complexity = 'low';
} else if (rawComplexity === 'complex' || rawComplexity === 'high') {
complexity = 'high';
}
return {
id: project.id,
name: project.name,
slug: project.slug || project.name.toLowerCase().replace(/\s+/g, '-'),
description: project.description || undefined,
status: project.status as ProjectStatus,
complexity,
progress: (project.progress as number) || 0,
openIssues: (project.openIssues as number) || project.issue_count || 0,
activeAgents: (project.activeAgents as number) || project.agent_count || 0,
currentSprint: project.active_sprint_name || undefined,
lastActivity: (project.lastActivity as string) || formatRelativeTime(project.updated_at),
createdAt: project.created_at,
owner: {
id: project.owner_id || 'unknown',
name: (project.ownerName as string) || 'Unknown',
},
tags: (project.tags as string[]) || [],
autonomyLevel: project.autonomy_level || 'milestone',
};
}
/**
* Format a date string as relative time (e.g., "2 minutes ago")
*/
function formatRelativeTime(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks > 1 ? 's' : ''} ago`;
return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`;
}
// ============================================================================
// Hook
@@ -206,35 +140,33 @@ export function useProjects(params: ProjectsListParams = {}) {
return useQuery<ProjectsListResponse>({
queryKey: ['projects', { search, status, complexity, sortBy, sortOrder, page, limit }],
queryFn: async () => {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 400));
// Call API via SDK (MSW intercepts in demo mode)
const response = await listProjectsApi({
query: {
status: status !== 'all' ? (status as ApiProjectStatus) : undefined,
search: search || undefined,
page,
limit,
},
});
// Filter projects
let filtered = [...mockProjects];
// Search filter
if (search) {
const searchLower = search.toLowerCase();
filtered = filtered.filter(
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower) ||
p.tags?.some((t) => t.toLowerCase().includes(searchLower))
);
if (response.error) {
throw new Error('Failed to fetch projects');
}
// Status filter
if (status !== 'all') {
filtered = filtered.filter((p) => p.status === status);
}
// Get raw response data
const apiData = response.data;
let projects = apiData.data.map((p) =>
mapProjectResponse(p as ProjectResponse & Record<string, unknown>)
);
// Complexity filter
// Client-side complexity filter (not supported by API)
if (complexity !== 'all') {
filtered = filtered.filter((p) => p.complexity === complexity);
projects = projects.filter((p) => p.complexity === complexity);
}
// Sort
filtered.sort((a, b) => {
// Client-side sorting
projects.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'name':
@@ -254,15 +186,12 @@ export function useProjects(params: ProjectsListParams = {}) {
return sortOrder === 'asc' ? comparison : -comparison;
});
// Pagination
const total = filtered.length;
// Calculate pagination
const total = apiData.pagination.total;
const totalPages = Math.ceil(total / limit);
const start = (page - 1) * limit;
const end = start + limit;
const paginatedData = filtered.slice(start, end);
return {
data: paginatedData,
data: projects,
pagination: {
page,
limit,

View File

@@ -0,0 +1,266 @@
/**
* Mock Project Data for Demo Mode
*
* Sample projects used by MSW handlers in demo mode.
*/
import type { ProjectResponse, ProjectStatus } from '@/lib/api/generated';
export interface ProjectListItem extends ProjectResponse {
// Extended UI fields (computed/stored separately in real app)
complexity?: 'script' | 'simple' | 'medium' | 'complex';
progress?: number;
openIssues?: number;
activeAgents?: number;
lastActivity?: string;
tags?: string[];
ownerName?: string;
}
export const sampleProjects: ProjectListItem[] = [
{
id: 'proj-001',
name: 'E-Commerce Platform Redesign',
slug: 'ecommerce-redesign',
description: 'Complete redesign of the e-commerce platform with modern UI/UX',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-15T10:00:00Z',
updated_at: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 70,
active_sprint_name: 'Sprint 3',
// Extended fields
complexity: 'complex',
progress: 67,
openIssues: 12,
activeAgents: 4,
lastActivity: '2 minutes ago',
tags: ['e-commerce', 'frontend', 'ux'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-002',
name: 'Mobile Banking App',
slug: 'mobile-banking',
description: 'Native mobile app for banking services with biometric authentication',
autonomy_level: 'full_control',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-11-20T09:00:00Z',
updated_at: new Date(Date.now() - 15 * 60 * 1000).toISOString(),
agent_count: 5,
issue_count: 45,
active_sprint_name: 'Sprint 2',
complexity: 'complex',
progress: 45,
openIssues: 8,
activeAgents: 5,
lastActivity: '15 minutes ago',
tags: ['mobile', 'fintech', 'security'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-003',
name: 'Internal HR Portal',
slug: 'hr-portal',
description: 'Employee self-service portal for HR operations',
autonomy_level: 'milestone',
status: 'paused',
settings: {},
owner_id: 'user-002',
created_at: '2025-10-01T08:00:00Z',
updated_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 25,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 23,
openIssues: 5,
activeAgents: 0,
lastActivity: '2 days ago',
tags: ['internal', 'hr', 'portal'],
ownerName: 'Maria Santos',
},
{
id: 'proj-004',
name: 'API Gateway Modernization',
slug: 'api-gateway',
description: 'Migrate legacy API gateway to cloud-native architecture',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-01T11:00:00Z',
updated_at: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 40,
active_sprint_name: 'Sprint 4',
complexity: 'complex',
progress: 82,
openIssues: 3,
activeAgents: 2,
lastActivity: '1 hour ago',
tags: ['api', 'cloud', 'infrastructure'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-005',
name: 'Customer Analytics Dashboard',
slug: 'analytics-dashboard',
description: 'Real-time analytics dashboard for customer behavior insights',
autonomy_level: 'milestone',
status: 'completed',
settings: {},
owner_id: 'user-003',
created_at: '2025-09-01T10:00:00Z',
updated_at: new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 50,
active_sprint_name: null,
complexity: 'medium',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '2 weeks ago',
tags: ['analytics', 'ml', 'dashboard'],
ownerName: 'Alex Johnson',
},
{
id: 'proj-006',
name: 'DevOps Pipeline Automation',
slug: 'devops-automation',
description: 'Automate CI/CD pipelines with AI-assisted deployments',
autonomy_level: 'milestone',
status: 'active',
settings: {},
owner_id: 'user-001',
created_at: '2025-12-10T14:00:00Z',
updated_at: new Date(Date.now() - 30 * 60 * 1000).toISOString(),
agent_count: 4,
issue_count: 30,
active_sprint_name: 'Sprint 1',
complexity: 'medium',
progress: 35,
openIssues: 6,
activeAgents: 3,
lastActivity: '30 minutes ago',
tags: ['devops', 'automation', 'ci-cd'],
ownerName: 'Felipe Cardoso',
},
{
id: 'proj-007',
name: 'Inventory Management System',
slug: 'inventory-system',
description: 'Warehouse inventory tracking with barcode scanning',
autonomy_level: 'full_control',
status: 'archived',
settings: {},
owner_id: 'user-002',
created_at: '2025-06-15T08:00:00Z',
updated_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
agent_count: 0,
issue_count: 80,
active_sprint_name: null,
complexity: 'simple',
progress: 100,
openIssues: 0,
activeAgents: 0,
lastActivity: '1 month ago',
tags: ['inventory', 'warehouse', 'logistics'],
ownerName: 'Maria Santos',
},
{
id: 'proj-008',
name: 'Customer Support Chatbot',
slug: 'support-chatbot',
description: 'AI-powered chatbot for 24/7 customer support',
autonomy_level: 'autonomous',
status: 'active',
settings: {},
owner_id: 'user-003',
created_at: '2025-12-05T09:00:00Z',
updated_at: new Date(Date.now() - 45 * 60 * 1000).toISOString(),
agent_count: 3,
issue_count: 35,
active_sprint_name: 'Sprint 2',
complexity: 'medium',
progress: 58,
openIssues: 4,
activeAgents: 2,
lastActivity: '45 minutes ago',
tags: ['ai', 'chatbot', 'support'],
ownerName: 'Alex Johnson',
},
];
// In-memory store for demo mode (allows create/update/delete)
let projectsStore = [...sampleProjects];
export function getProjects(): ProjectListItem[] {
return projectsStore;
}
export function getProjectById(id: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.id === id);
}
export function getProjectBySlug(slug: string): ProjectListItem | undefined {
return projectsStore.find((p) => p.slug === slug);
}
export function createProject(data: Partial<ProjectListItem>): ProjectListItem {
const newProject: ProjectListItem = {
id: `proj-${Date.now()}`,
name: data.name || 'New Project',
slug: data.slug || data.name?.toLowerCase().replace(/\s+/g, '-') || `project-${Date.now()}`,
description: data.description || null,
autonomy_level: data.autonomy_level || 'milestone',
status: 'active',
settings: data.settings || {},
owner_id: data.owner_id || 'user-001',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
agent_count: 0,
issue_count: 0,
active_sprint_name: null,
complexity: data.complexity || 'medium',
progress: 0,
openIssues: 0,
activeAgents: 0,
lastActivity: 'Just now',
tags: data.tags || [],
ownerName: 'Demo User',
};
projectsStore.unshift(newProject);
return newProject;
}
export function updateProject(
id: string,
data: Partial<ProjectListItem>
): ProjectListItem | undefined {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return undefined;
projectsStore[index] = {
...projectsStore[index],
...data,
updated_at: new Date().toISOString(),
};
return projectsStore[index];
}
export function deleteProject(id: string): boolean {
const index = projectsStore.findIndex((p) => p.id === id);
if (index === -1) return false;
projectsStore.splice(index, 1);
return true;
}
export function resetProjects(): void {
projectsStore = [...sampleProjects];
}

View File

@@ -8,7 +8,7 @@
*
* For custom handler behavior, use src/mocks/handlers/overrides.ts
*
* Generated: 2025-12-30T02:14:59.598Z
* Generated: 2026-01-03T01:13:34.961Z
*/
import { http, HttpResponse, delay } from 'msw';
@@ -603,4 +603,544 @@ export const generatedHandlers = [
message: 'Operation successful'
});
}),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Projects
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Archive Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Agent Type
*/
http.post(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Agent Types
*/
http.get(`${API_BASE_URL}/api/v1/agent-types`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent Type
*/
http.patch(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Deactivate Agent Type
*/
http.delete(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/:agent_type_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Type by Slug
*/
http.get(`${API_BASE_URL}/api/v1/agent-types/slug/:slug`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue Statistics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/stats`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Issue
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Issue
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Assign Issue
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assign`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Unassign Issue
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/assignment`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Trigger Issue Sync
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/issues/:issue_id/sync`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Spawn Agent Instance
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Project Agents
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Agent
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Terminate Agent
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Pause Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/pause`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Resume Agent
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/resume`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Agent Metrics
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/agents/:agent_id/metrics`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Create Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* List Sprints
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Active Sprint
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/active`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Project Velocity
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/velocity`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Details
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Update Sprint
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Delete Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Start Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/start`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Complete Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/complete`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Cancel Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/cancel`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Get Sprint Issues
*/
http.get(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Add Issue to Sprint
*/
http.post(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
/**
* Remove Issue from Sprint
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:project_id/sprints/:sprint_id/issues`, async ({ request, params }) => {
await delay(NETWORK_DELAY);
return HttpResponse.json({
success: true,
message: 'Operation successful'
});
}),
];

View File

@@ -14,6 +14,14 @@
import { http, HttpResponse, delay } from 'msw';
import { generateMockToken } from '../utils/tokens';
import { validateCredentials, setCurrentUser, currentUser } from '../data/users';
import {
getProjects,
getProjectById,
getProjectBySlug,
createProject,
updateProject,
deleteProject,
} from '../data/projects';
import config from '@/config/app.config';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8000';
@@ -182,4 +190,164 @@ export const overrideHandlers = [
accounts: [],
});
}),
// ============================================
// PROJECT HANDLERS
// ============================================
/**
* List Projects
* Returns paginated list of projects with filtering support
*/
http.get(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const url = new URL(request.url);
const status = url.searchParams.get('status');
const search = url.searchParams.get('search');
const skip = parseInt(url.searchParams.get('skip') || '0');
const limit = parseInt(url.searchParams.get('limit') || '20');
let projects = getProjects();
// Filter by status
if (status && status !== 'all') {
projects = projects.filter((p) => p.status === status);
}
// Filter by search term
if (search) {
const searchLower = search.toLowerCase();
projects = projects.filter(
(p) =>
p.name.toLowerCase().includes(searchLower) ||
p.description?.toLowerCase().includes(searchLower)
);
}
const total = projects.length;
const paginatedProjects = projects.slice(skip, skip + limit);
const totalPages = Math.ceil(total / limit);
return HttpResponse.json({
data: paginatedProjects,
pagination: {
total,
page: Math.floor(skip / limit) + 1,
limit,
total_pages: totalPages,
has_next: skip + limit < total,
has_prev: skip > 0,
},
});
}),
/**
* Create Project
*/
http.post(`${API_BASE_URL}/api/v1/projects`, async ({ request }) => {
await delay(NETWORK_DELAY);
const body = (await request.json()) as any;
const newProject = createProject(body);
return HttpResponse.json(newProject, { status: 201 });
}),
/**
* Get Project by ID
*/
http.get(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const project = getProjectById(projectId as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Get Project by Slug
*/
http.get(`${API_BASE_URL}/api/v1/projects/slug/:slug`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { slug } = params;
const project = getProjectBySlug(slug as string);
if (!project) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(project);
}),
/**
* Update Project
*/
http.patch(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params, request }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const body = (await request.json()) as any;
const updated = updateProject(projectId as string, body);
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Archive (Delete) Project
*/
http.delete(`${API_BASE_URL}/api/v1/projects/:projectId`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const deleted = deleteProject(projectId as string);
if (!deleted) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json({ message: 'Project archived successfully' });
}),
/**
* Pause Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/pause`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'paused' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
/**
* Resume Project
*/
http.post(`${API_BASE_URL}/api/v1/projects/:projectId/resume`, async ({ params }) => {
await delay(NETWORK_DELAY);
const { projectId } = params;
const updated = updateProject(projectId as string, { status: 'active' as any });
if (!updated) {
return HttpResponse.json({ detail: 'Project not found' }, { status: 404 });
}
return HttpResponse.json(updated);
}),
];

View File

@@ -10,6 +10,7 @@ describe('ProjectCard', () => {
const mockProject: ProjectListItem = {
id: 'proj-1',
name: 'Test Project',
slug: 'test-project',
description: 'This is a test project description',
status: 'active',
complexity: 'medium',
@@ -21,6 +22,7 @@ describe('ProjectCard', () => {
createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'Test User' },
tags: ['frontend', 'react', 'typescript'],
autonomyLevel: 'milestone',
};
it('renders project name', () => {

View File

@@ -18,6 +18,7 @@ describe('ProjectsGrid', () => {
{
id: 'proj-1',
name: 'Project One',
slug: 'project-one',
description: 'First project',
status: 'active',
complexity: 'medium',
@@ -27,10 +28,12 @@ describe('ProjectsGrid', () => {
lastActivity: '5 min ago',
createdAt: '2025-01-01T00:00:00Z',
owner: { id: 'user-1', name: 'User One' },
autonomyLevel: 'milestone',
},
{
id: 'proj-2',
name: 'Project Two',
slug: 'project-two',
description: 'Second project',
status: 'paused',
complexity: 'high',
@@ -40,6 +43,7 @@ describe('ProjectsGrid', () => {
lastActivity: '1 day ago',
createdAt: '2025-01-02T00:00:00Z',
owner: { id: 'user-2', name: 'User Two' },
autonomyLevel: 'full_control',
},
];