Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6ef2617
chore(scaffold): create feat/scaffold branch from main
ed-morais May 10, 2026
fd70368
chore(frontend): initialize Vite + React (TS) + Tailwind
ed-morais May 10, 2026
6e1685f
chore(backend): initialize Django 5.x project with DRF
ed-morais May 10, 2026
b9e6ce8
chore(infra): add Docker Compose with PostgreSQL 16 and Redis
ed-morais May 10, 2026
c1b1ea3
chore(config): configure CORS, JWT, and environment variable loading
ed-morais May 10, 2026
fc7b6d1
chore(test): configure pytest-django, factory-boy, pytest-mock
ed-morais May 10, 2026
7fcb0e2
chore(test): configure Vitest, React Testing Library, and MSW
ed-morais May 10, 2026
67544ba
chore(test): configure Playwright for E2E tests
ed-morais May 10, 2026
54f924d
ci: add GitHub Actions workflow for backend and frontend tests
ed-morais May 10, 2026
2576749
feat(health): implement health check endpoint — TDD GREEN
ed-morais May 10, 2026
02c4b9f
feat(scaffold): first functional test passing — Phase 0 complete
ed-morais May 10, 2026
623b206
chore(config): update AGENT_CONTEXT — Phase 0 complete, all 11 steps …
ed-morais May 10, 2026
65dd98e
fix(test): exclude e2e/ from Vitest test discovery
ed-morais May 10, 2026
ab50602
fix(config): add token_blacklist to INSTALLED_APPS
ed-morais May 10, 2026
74def78
fix(test): functional test inherits from FunctionalTest base class
ed-morais May 10, 2026
e1467e1
fix(test): strengthen App test to assert heading is rendered
ed-morais May 10, 2026
843d45f
test(health): add comprehensive unit tests for health endpoint
ed-morais May 10, 2026
e1127d6
chore(config): update AGENT_CONTEXT — Phase 0 PR created
ed-morais May 10, 2026
12d97a3
chore(config): reset AGENT_CONTEXT to idle — Phase 0 complete
ed-morais May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 297 additions & 24 deletions .claude/AGENT_CONTEXT.md

Large diffs are not rendered by default.

84 changes: 84 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
backend-tests:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16
env:
POSTGRES_DB: ima_test
POSTGRES_USER: ima
POSTGRES_PASSWORD: ima
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.10'

- name: Install dependencies
run: |
cd backend
pip install -r requirements.txt -r requirements-test.txt

- name: Run tests
env:
DATABASE_URL: postgres://ima:ima@localhost:5432/ima_test
REDIS_URL: redis://localhost:6379/0
SECRET_KEY: test-secret-key-for-ci-only
DEBUG: "true"
ALLOWED_HOSTS: localhost,127.0.0.1
FRONTEND_URL: http://localhost:5173
AI_DEFAULT_PROVIDER: gemini
GEMINI_API_KEY: test-key
run: |
cd backend
pytest --cov=. --cov-report=xml -x

- uses: codecov/codecov-action@v4
with:
file: backend/coverage.xml

frontend-tests:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: cd frontend && npm ci

- name: Run unit tests
run: cd frontend && npm run test -- --run

- name: Install Playwright browsers
run: cd frontend && npx playwright install --with-deps chromium

- name: Run E2E tests
run: cd frontend && npx playwright test
22 changes: 22 additions & 0 deletions backend/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest
from django.test import Client as DjangoClient
from rest_framework.test import APIClient


@pytest.fixture
def client(db):
"""A basic Django test client with database access."""
return DjangoClient()


@pytest.fixture
def api_client(db):
"""An unauthenticated DRF API client with database access."""
return APIClient()


@pytest.fixture
def mock_ai(mocker):
"""Mock AI provider that returns predictable responses."""
mock = mocker.patch('ai.providers.get_ai_provider', create=True)
return mock
1 change: 1 addition & 0 deletions backend/functional_tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

11 changes: 11 additions & 0 deletions backend/functional_tests/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Base class for functional tests."""
from django.test import TestCase


class FunctionalTest(TestCase):
"""
Base class for all functional tests.
Uses Django's TestCase (in-process test client) for Phase 0.
Will be upgraded to LiveServerTestCase when Playwright FTs are added.
"""
pass
23 changes: 23 additions & 0 deletions backend/functional_tests/test_homepage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
Functional Test: Homepage / Health Check

Story: A developer sets up Ima for the first time.
They confirm the backend health check is reachable and returns
a well-formed JSON response with status "ok".
"""
from functional_tests.base import FunctionalTest


class HomepageTest(FunctionalTest):
"""
Story: A developer sets up Ima for the first time.
They confirm the backend is alive end-to-end.
"""

def test_user_visits_ima_and_sees_landing_page(self):
"""Health check returns 200 with status ok and no error."""
response = self.client.get('/api/v1/health/')
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data['data']['status'], 'ok')
self.assertIsNone(data['error'])
Empty file added backend/ima/__init__.py
Empty file.
16 changes: 16 additions & 0 deletions backend/ima/asgi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""
ASGI config for ima project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ima.settings')

application = get_asgi_application()
150 changes: 150 additions & 0 deletions backend/ima/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
Django settings for ima project.
"""

from pathlib import Path
from datetime import timedelta
import dj_database_url
from decouple import config

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

# Security
SECRET_KEY = config('SECRET_KEY', default='django-insecure-dev-key-change-in-production')
DEBUG = config('DEBUG', default=True, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost,127.0.0.1').split(',')

# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third-party
'corsheaders',
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',
'drf_spectacular',
'channels',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'corsheaders.middleware.CorsMiddleware', # Must be before CommonMiddleware
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'ima.urls'

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]

WSGI_APPLICATION = 'ima.wsgi.application'
ASGI_APPLICATION = 'ima.asgi.application'

# Database
DATABASE_URL = config(
'DATABASE_URL',
default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'
)
DATABASES = {
'default': dj_database_url.config(
default=DATABASE_URL,
conn_max_age=600,
)
}

# Redis / Celery
REDIS_URL = config('REDIS_URL', default='redis://localhost:6379/0')
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL

# Password validation
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]

# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True

# Static files
STATIC_URL = 'static/'

# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# CORS
FRONTEND_URL = config('FRONTEND_URL', default='http://localhost:5173')
CORS_ALLOWED_ORIGINS = [
FRONTEND_URL,
'http://localhost:5173',
'http://127.0.0.1:5173',
]
CORS_ALLOW_CREDENTIALS = True

# Django REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}

# JWT
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'SIGNING_KEY': SECRET_KEY,
'AUTH_HEADER_TYPES': ('Bearer',),
}

# AI Providers
AI_DEFAULT_PROVIDER = config('AI_DEFAULT_PROVIDER', default='gemini')
AI_MODEL = config('AI_MODEL', default='gemini-2.5-flash')
GEMINI_API_KEY = config('GEMINI_API_KEY', default='')
ANTHROPIC_API_KEY = config('ANTHROPIC_API_KEY', default='')
OPENAI_API_KEY = config('OPENAI_API_KEY', default='')

# DRF Spectacular (OpenAPI)
SPECTACULAR_SETTINGS = {
'TITLE': 'Ima API',
'DESCRIPTION': 'AI-powered personal guidance for ADHD brains',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
1 change: 1 addition & 0 deletions backend/ima/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading
Loading