Project Management
Creating and managing test projects
📋 Overview
Projects are the core entity in Izri. Each project represents a GitHub repository that you want to generate and run tests for. Projects can be owned by individual users or organizations, and include settings for test generation preferences.
🎯 User Stories
- As a developer, I want to add my repository so I can generate tests for it
- As a team lead, I want to manage multiple projects under an organization
- As a user, I want to configure test generation preferences per project
- As a developer, I want to track which branch to analyze for each project
🏗️ Architecture
Database Schema
projects table:
{
id: string // Primary key (ULID)
name: string // Project display name
description: string? // Optional description
repository: string // GitHub repository URL
branch: string // Branch to analyze (default: 'main')
languages: string[] // Array of programming languages used
frameworks: string[] // Array of frameworks/libraries used
userId: string // Owner user ID (ULID)
organizationId: string // Required organization ID (ULID)
createdAt: Date
updatedAt: Date
}
projectSettings table:
{
id: string
projectId: string
enableUnitTests: boolean // Generate unit tests (default: true)
enableIntegrationTests: boolean // Generate integration tests (default: true)
enableE2ETests: boolean // Generate E2E tests (default: true)
autoRunTests: boolean // Automatically run after generation (default: false)
notifyOnFailure: boolean // Send notifications on test failures (default: true)
aiProvider: string? // OpenAI, Anthropic, etc.
aiModel: string? // gpt-4, claude-3-opus, etc.
createdAt: Date
updatedAt: Date
}
Indexes
CREATE INDEX projects_user_idx ON projects(user_id);
CREATE INDEX projects_org_idx ON projects(organization_id);
🔌 API Reference
Create Project
// tRPC endpoint: projects.createProject
const result = await trpc.projects.createProject.mutate({
name: 'My Project',
description: 'Description of my project',
repository: 'https://github.com/username/repo',
branch: 'main',
languages: ['typescript'],
frameworks: ['react'],
organizationId: 'org_123',
userEmail: 'user@example.com'
})
// Response
{
project: {
id: 'proj_123',
name: 'My Project',
description: 'Description of my project',
repository: 'https://github.com/username/repo',
branch: 'main',
languages: ['typescript'],
frameworks: ['react'],
userId: 'user_123',
organizationId: 'org_123',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
testRuns: []
}
}
Validation:
- Project name required (min 1 character)
- Repository URL required
- At least one language required
- Organization ID required (ULID format)
- Valid email required
- Duplicate repository per user not allowed
- Automatically creates default project settings
Get Projects
// Get all projects for current user (optionally filtered by organization)
const projects = await trpc.projects.getProjects.query({
userEmail: 'user@example.com',
organizationId: 'org_123' // optional
})
// Response
{
projects: [
{
id: 'proj_123',
name: 'My Project',
repository: 'https://github.com/username/repo',
languages: ['typescript'],
frameworks: ['react'],
createdAt: '2024-01-01T00:00:00Z',
testRuns: [
{
id: 'run_456',
status: 'COMPLETED',
passedTests: 50,
failedTests: 2,
totalTests: 52,
createdAt: '2024-01-02T00:00:00Z'
}
]
}
]
}
Features:
- Includes latest test run per project
- Ordered by updated date (newest first)
- Filtered by user ownership and optionally by organization
Get Project
// Get single project with full details
const project = await trpc.projects.getProject.query({
projectId: 'proj_123',
userEmail: 'user@example.com'
})
// Response
{
project: {
id: 'proj_123',
name: 'My Project',
description: 'Description',
repository: 'https://github.com/username/repo',
branch: 'main',
languages: ['typescript'],
frameworks: ['react'],
userId: 'user_123',
organizationId: 'org_123',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z',
testRuns: [...],
settings: {
enableUnitTests: true,
enableIntegrationTests: true,
enableE2ETests: true,
autoRunTests: false,
notifyOnFailure: true,
aiProvider: 'openai',
aiModel: 'gpt-4'
}
}
}
Update Project
// Update project details
const result = await trpc.projects.updateProject.mutate({
projectId: 'proj_123',
userEmail: 'user@example.com',
name: 'Updated Project Name',
description: 'Updated description',
branch: 'develop',
languages: ['typescript', 'javascript'],
frameworks: ['next.js', 'react']
})
// Response: Updated project object
Updatable fields:
- name
- description
- branch
- languages (array)
- frameworks (array)
Non-updatable:
- id
- repository (would be a different project)
- userId
- organizationId
- createdAt
Delete Project
// Delete project (soft or hard delete)
await trpc.projects.deleteProject.mutate({
projectId: 'proj_123'
})
// Response
{
success: true
}
Cascade behavior:
- Deletes associated project settings
- Deletes associated test runs
- Deletes associated analyses
- Cannot be undone
Update Project Settings
// Update test generation preferences
await trpc.projects.updateProjectSettings.mutate({
projectId: 'proj_123',
userEmail: 'user@example.com',
autoRunTests: true,
enableE2ETests: true,
enableIntegrationTests: false,
enableUnitTests: true,
notifyOnFailure: true,
aiProvider: 'anthropic',
aiModel: 'claude-3-opus-20240229'
})
Get Project Analyses
// Get all analyses for a project
const analyses = await trpc.projects.getProjectAnalyses.query({
projectId: 'proj_123',
userEmail: 'user@example.com'
})
// Response
{
analyses: [
{
id: 'analysis_456',
projectId: 'proj_123',
status: 'COMPLETED',
summary: 'Analysis completed successfully',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
}
]
}
Features:
- Returns all analyses for the project
- Ordered by creation date (newest first)
- Requires project ownership verification
Get Latest Project Analysis
// Get the most recent analysis for a project
const analysis = await trpc.projects.getLatestProjectAnalysis.query({
projectId: 'proj_123',
userEmail: 'user@example.com'
})
// Response
{
analysis: {
id: 'analysis_456',
projectId: 'proj_123',
status: 'COMPLETED',
summary: 'Analysis completed successfully',
createdAt: '2024-01-01T00:00:00Z',
updatedAt: '2024-01-01T00:00:00Z'
} // or null if no analyses exist
}
Features:
- Returns the most recent analysis only
- Returns null if no analyses exist
- Requires project ownership verification
🎨 Frontend Implementation
Project List Page
Route: /organizations/$slug/projects
import { trpc } from '~/lib/trpc'
export default function ProjectsPage() {
const { slug } = useParams()
const { data, isLoading } = trpc.projects.getProjects.useQuery({
userEmail: user.email,
organizationId: currentOrganization?.id
})
if (isLoading) return <ProjectsSkeleton />
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Projects</h1>
<Button asChild>
<Link to={`/organizations/${slug}/projects/new`}>
<PlusIcon /> New Project
</Link>
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{data?.projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</div>
)
}
Project Card Component
interface ProjectCardProps {
project: {
id: string
name: string
repository: string
languages: string[]
frameworks: string[]
testRuns: Array<{ status: string; passedTests: number; totalTests: number }>
}
}
function ProjectCard({ project }: ProjectCardProps) {
const { slug } = useParams()
const latestRun = project.testRuns[0]
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>{project.name}</CardTitle>
<div className="flex gap-1">
{project.languages.slice(0, 2).map((lang) => (
<Badge key={lang} variant="outline" className="text-xs">
{lang}
</Badge>
))}
{project.languages.length > 2 && (
<Badge variant="outline" className="text-xs">
+{project.languages.length - 2}
</Badge>
)}
</div>
</div>
<CardDescription className="truncate">
{project.repository}
</CardDescription>
</CardHeader>
<CardContent>
{project.frameworks.length > 0 && (
<div className="text-sm text-muted-foreground mb-2">
Frameworks: {project.frameworks.join(', ')}
</div>
)}
{latestRun && (
<div className="flex items-center gap-2">
<StatusBadge status={latestRun.status} />
<span className="text-sm">
{latestRun.passedTests}/{latestRun.totalTests} tests passed
</span>
</div>
)}
</CardContent>
<CardFooter>
<Button asChild variant="outline" className="w-full">
<Link to={`/organizations/${slug}/projects/${project.id}`}>
View Details
</Link>
</Button>
</CardFooter>
</Card>
)
}
Create Project Form
Route: /organizations/$slug/projects/new
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
const projectSchema = z.object({
name: z.string().min(1, 'Project name is required'),
description: z.string().optional(),
repository: z.string().url('Must be a valid URL'),
branch: z.string().default('main'),
languages: z.array(z.string()).min(1, 'At least one language is required'),
frameworks: z.array(z.string()).optional(),
})
export default function NewProjectPage() {
const { slug } = useParams()
const navigate = useNavigate()
const user = useAuth()
const currentOrganization = useCurrentOrganization()
const form = useForm({
resolver: zodResolver(projectSchema),
defaultValues: {
branch: 'main',
languages: [],
frameworks: []
}
})
const createProject = trpc.projects.createProject.useMutation({
onSuccess: (data) => {
navigate(`/organizations/${slug}/projects/${data.project.id}`)
}
})
const onSubmit = (values: z.infer<typeof projectSchema>) => {
createProject.mutate({
...values,
organizationId: currentOrganization?.id || '',
userEmail: user.email
})
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Project Name *</FormLabel>
<FormControl>
<Input {...field} placeholder="My Awesome Project" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="repository"
render={({ field }) => (
<FormItem>
<FormLabel>Repository URL *</FormLabel>
<FormControl>
<Input {...field} placeholder="https://github.com/username/repo" />
</FormControl>
<FormDescription>
GitHub repository URL for analysis and testing
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="branch"
render={({ field }) => (
<FormItem>
<FormLabel>Default Branch</FormLabel>
<FormControl>
<Input {...field} placeholder="main" />
</FormControl>
<FormDescription>
Branch to analyze for test generation
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="languages"
render={({ field }) => (
<FormItem>
<FormLabel>Languages *</FormLabel>
<FormControl>
<MultiSelect
{...field}
options={LANGUAGES}
placeholder="Select programming languages..."
/>
</FormControl>
<FormDescription>
Languages used in your project
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="frameworks"
render={({ field }) => (
<FormItem>
<FormLabel>Frameworks & Libraries</FormLabel>
<FormControl>
<MultiSelect
{...field}
options={FRAMEWORKS}
placeholder="Select frameworks..."
/>
</FormControl>
<FormDescription>
Frameworks and libraries used in your project (optional)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end space-x-4 pt-6">
<Button asChild variant="outline">
<Link to={`/organizations/${slug}/projects`}>Cancel</Link>
</Button>
<Button type="submit" disabled={createProject.isLoading}>
{createProject.isLoading && <LoaderIcon className="animate-spin" />}
Create Project
</Button>
</div>
</form>
</Form>
)
}
Project Details Page
Route: /organizations/$slug/projects/$id
export default function ProjectDetailsPage() {
const { slug, id } = useParams()
const { data, isLoading } = trpc.projects.getProject.useQuery({
projectId: id!,
userEmail: user.email
})
if (isLoading) return <Skeleton />
const project = data?.project
return (
<div className="space-y-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold">{project.name}</h1>
<p className="text-muted-foreground">{project.description}</p>
</div>
<Button asChild variant="outline">
<Link to={`/organizations/${slug}/projects/${id}/settings`}>
<SettingsIcon /> Settings
</Link>
</Button>
</div>
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="tests">Test Runs</TabsTrigger>
<TabsTrigger value="analysis">Analysis</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<ProjectOverview project={project} />
</TabsContent>
<TabsContent value="tests">
<TestRunsList projectId={id} />
</TabsContent>
<TabsContent value="analysis">
<AnalysisHistory projectId={id} />
</TabsContent>
</Tabs>
</div>
)
}
✅ Testing
Unit Tests
import { describe, expect, it } from 'vitest'
import { createCaller } from '../context'
describe('projects.createProject', () => {
it('creates a project with valid input', async () => {
const caller = await createCaller({ user: testUser })
const result = await caller.projects.createProject({
name: 'Test Project',
repository: 'https://github.com/test/repo',
branch: 'main',
languages: ['typescript'],
frameworks: ['react'],
organizationId: 'org_123',
userEmail: testUser.email
})
expect(result.project).toBeDefined()
expect(result.project.name).toBe('Test Project')
expect(result.project.languages).toEqual(['typescript'])
expect(result.project.frameworks).toEqual(['react'])
expect(result.project.organizationId).toBe('org_123')
expect(result.project.userId).toBe(testUser.id)
})
it('throws error for duplicate repository', async () => {
const caller = await createCaller({ user: testUser })
// Create first project
await caller.projects.createProject({
name: 'Project 1',
repository: 'https://github.com/test/repo',
languages: ['typescript'],
organizationId: 'org_123',
userEmail: testUser.email
})
// Try to create duplicate
await expect(
caller.projects.createProject({
name: 'Project 2',
repository: 'https://github.com/test/repo',
languages: ['typescript'],
organizationId: 'org_123',
userEmail: testUser.email
})
).rejects.toThrow('Project with this repository already exists')
})
it('requires at least one language', async () => {
const caller = await createCaller({ user: testUser })
await expect(
caller.projects.createProject({
name: 'Test Project',
repository: 'https://github.com/test/repo',
languages: [],
organizationId: 'org_123',
userEmail: testUser.email
})
).rejects.toThrow('At least one language is required')
})
})
Integration Tests
describe('Project lifecycle', () => {
it('creates, updates, and deletes a project', async () => {
// Create
const created = await trpc.projects.createProject.mutate({
name: 'Test Project',
repository: 'https://github.com/test/repo',
languages: ['typescript'],
frameworks: ['react'],
organizationId: 'org_123',
userEmail: user.email
})
expect(created.project.id).toBeDefined()
const projectId = created.project.id
// Update
await trpc.projects.updateProject.mutate({
projectId,
userEmail: user.email,
name: 'Updated Name',
languages: ['typescript', 'javascript']
})
const updated = await trpc.projects.getProject.query({
projectId,
userEmail: user.email
})
expect(updated.project.name).toBe('Updated Name')
expect(updated.project.languages).toEqual(['typescript', 'javascript'])
// Delete
await trpc.projects.deleteProject.mutate({
projectId,
userEmail: user.email
})
await expect(
trpc.projects.getProject.query({
projectId,
userEmail: user.email
})
).rejects.toThrow()
})
})
🔗 Related Documentation
- Repository Analysis: How projects are analyzed
- Test Generation: Generating tests for projects
- Organizations: Multi-tenancy support
- API Reference: Complete API docs
Next: Repository Analysis →