Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: WorkSynapse CI

on:
push:
branches: [ "main", "develop" ]
pull_request:
branches: [ "main", "develop" ]

jobs:
test:
runs-on: ubuntu-latest
env:
PROJECT_NAME: "WorkSynapse API Test"
API_V1_STR: "/api/v1"
SECRET_KEY: "test_secret_key_for_ci_pipeline_only"
SERVICE_API_KEY: "test_service_key"
POSTGRES_USER: "test"
POSTGRES_PASSWORD: "test"
POSTGRES_DB: "test_db"
POSTGRES_HOST: "localhost"
POSTGRES_PORT: 5432
DATABASE_URL: "sqlite+aiosqlite:///:memory:"
PYTHONPATH: ${{ github.workspace }}/backend

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.12
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-asyncio pytest-cov httpx faker aiosqlite sqlalchemy
pip install -r backend/requirements.txt

- name: Run Tests
run: |
cd backend
pytest tests/ --cov=app --cov-report=xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
51 changes: 30 additions & 21 deletions backend/app/api/v1/routers/user_activity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Any, Dict, List

from app.api import deps
Expand All @@ -9,12 +10,13 @@
from app.models.task.model import Task
from app.models.note.model import Note
from app.models.worklog.model import WorkLog, ActivityLog
from app.models.agent_chat.model import AgentConversation

router = APIRouter()

@router.get("/", response_model=Dict[str, List[Any]])
def get_user_activity(
db: Session = Depends(deps.get_db),
async def get_user_activity(
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_active_user),
skip: int = 0,
limit: int = 100,
Expand All @@ -31,40 +33,44 @@ def format_list(items):
]

# 1. Agents Created
agents = db.query(Agent).filter(Agent.user_id == current_user.id).all()
result = await db.execute(select(Agent).filter(Agent.created_by_user_id == current_user.id))
agents = result.scalars().all()

# 2. Projects Created (Owned)
projects = db.query(Project).filter(Project.owner_id == current_user.id).all()
result = await db.execute(select(Project).filter(Project.owner_id == current_user.id))
projects = result.scalars().all()

# 3. Tasks Created
# Note: Using created_by_user_id based on User model relationship
try:
tasks = db.query(Task).filter(Task.created_by_user_id == current_user.id).limit(limit).all()
result = await db.execute(select(Task).filter(Task.created_by_id == current_user.id).limit(limit))
tasks = result.scalars().all()
except Exception:
# Fallback if field name differs (e.g. might be user_id or creator_id)
# Using a safer generic try/except to avoid crashing the whole endpoint
tasks = []

# 4. Notes Created
try:
notes = db.query(Note).filter(Note.owner_id == current_user.id).limit(limit).all()
result = await db.execute(select(Note).filter(Note.owner_id == current_user.id).limit(limit))
notes = result.scalars().all()
except Exception:
notes = []

# 5. Agent Sessions
# 5. Agent Conversations
try:
sessions = db.query(AgentSession).filter(AgentSession.user_id == current_user.id).limit(limit).all()
result = await db.execute(select(AgentConversation).filter(AgentConversation.user_id == current_user.id).limit(limit))
sessions = result.scalars().all()
except Exception:
sessions = []

# 6. Work Logs
try:
work_logs = db.query(WorkLog).filter(WorkLog.user_id == current_user.id).limit(limit).all()
result = await db.execute(select(WorkLog).filter(WorkLog.user_id == current_user.id).limit(limit))
work_logs = result.scalars().all()
except Exception:
work_logs = []

# 7. Activity Logs
activity_logs = db.query(ActivityLog).filter(ActivityLog.user_id == current_user.id).order_by(ActivityLog.timestamp.desc()).limit(limit).all()
result = await db.execute(select(ActivityLog).filter(ActivityLog.user_id == current_user.id).order_by(ActivityLog.created_at.desc()).limit(limit))
activity_logs = result.scalars().all()

return {
"agents": format_list(agents),
Expand All @@ -77,20 +83,23 @@ def format_list(items):
}

@router.get("/logs", response_model=List[Any])
def get_user_activity_logs(
db: Session = Depends(deps.get_db),
async def get_user_activity_logs(
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_active_user),
skip: int = 0,
limit: int = 50,
) -> Any:
"""
Get specific activity logs for the user (audit trail).
"""
logs = db.query(ActivityLog).filter(ActivityLog.user_id == current_user.id)\
.order_by(ActivityLog.timestamp.desc())\
.offset(skip)\
.limit(limit)\
.all()
result = await db.execute(
select(ActivityLog)
.filter(ActivityLog.user_id == current_user.id)
.order_by(ActivityLog.created_at.desc())
.offset(skip)
.limit(limit)
)
logs = result.scalars().all()

return [
{k: v for k, v in log.__dict__.items() if not k.startswith('_')}
Expand Down
1 change: 0 additions & 1 deletion backend/app/models/activity/__init__.py

This file was deleted.

46 changes: 0 additions & 46 deletions backend/app/models/activity/model.py

This file was deleted.

4 changes: 2 additions & 2 deletions backend/app/models/agent_builder/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ class CustomAgent(Base, AuditMixin):
__tablename__ = "custom_agents"
__table_args__ = (
UniqueConstraint('name', 'created_by_user_id', name='uq_custom_agent_name_user'),
Index('ix_custom_agents_status', 'status'),
Index('ix_custom_agents_creator', 'created_by_user_id'),
# Index('ix_custom_agents_status', 'status'), # Defined in mapped_column
# Index('ix_custom_agents_creator', 'created_by_user_id'), # Defined in AuditMixin
)

id: Mapped[int] = mapped_column(Integer, primary_key=True)
Expand Down
9 changes: 9 additions & 0 deletions backend/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[pytest]
asyncio_mode = auto
asyncio_default_fixture_loop_scope = session
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --cov=app --cov-report=term-missing
env_files =
.env
46 changes: 46 additions & 0 deletions backend/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# WorkSynapse Testing

This directory contains the comprehensive test suite for the WorkSynapse backend.

## Structure

- `fixtures/`: Reusable test data factories (Users, Notes, Projects, Agents).
- `mocks/`: Mock implementations for external services (OpenAI, Google, Slack).
- `load_test/`: Locust load testing scripts.
- `conftest.py`: Global test configuration, database setup, and fixtures.

## Running Tests

1. **Install Dependencies:**
```bash
pip install -r requirements.txt
pip install pytest pytest-asyncio pytest-cov httpx faker aiosqlite
```

2. **Run All Tests:**
```bash
pytest
```

3. **Run with Coverage:**
```bash
pytest --cov=app --cov-report=html
```
Open `htmlcov/index.html` to view the report.

## Load Testing

1. **Install Locust:**
```bash
pip install locust
```

2. **Run Locust:**
```bash
locust -f tests/load_test/locustfile.py
```
Open http://localhost:8089 in your browser.

## Continuous Integration

The project includes a GitHub Actions workflow `.github/workflows/ci.yml` that automatically runs tests on push/PR.
Empty file added backend/tests/__init__.py
Empty file.
Loading
Loading