diff --git a/.pi-map.index.md b/.pi-map.index.md index 0594587..6c615df 100644 --- a/.pi-map.index.md +++ b/.pi-map.index.md @@ -16,7 +16,7 @@ dir: . Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling. +Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions. ## parent - ## children diff --git a/.pi-map.md b/.pi-map.md index 13e05e0..451d643 100644 --- a/.pi-map.md +++ b/.pi-map.md @@ -18,7 +18,7 @@ index: ./.pi-map.index.md Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration package for a self-hosted project management platform with OAuth2 authentication, providing Docker orchestration, environment templates, and development tooling. +Infrastructure and deployment configuration package for a self-hosted project management platform with containerized services, reverse proxy support, and AI agent workflow definitions. ## files - .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment - .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git @@ -31,7 +31,7 @@ Infrastructure and deployment configuration package for a self-hosted project ma - progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases - swap-pane | Empty file with no functionality ## arch -Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy for TLS/ingress, and environment-driven configuration; includes Python/Node multi-language backend-frontend split with Makefile-driven lifecycle management. +Docker Compose-based microservices architecture with PostgreSQL/Redis backend, separate API/frontend containers, Traefik reverse proxy integration, environment-driven configuration, and OpenSpec-driven AI agent orchestration. ## tags docker, redis, git, application, postgresql, compose, traefik, project ## symbols diff --git a/apps/.pi-map.index.md b/apps/.pi-map.index.md index 169ed5a..a814c2f 100644 --- a/apps/.pi-map.index.md +++ b/apps/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps ## role -Contains the top-level application entry points and executable binaries for the project. +Contains the main deployable application modules that compose the complete system. ## parent index: ./.pi-map.index.md map: ./.pi-map.md diff --git a/apps/.pi-map.md b/apps/.pi-map.md index 89e62dd..132ccd6 100644 --- a/apps/.pi-map.md +++ b/apps/.pi-map.md @@ -4,10 +4,10 @@ dir: apps index: apps/.pi-map.index.md ## role -Contains the top-level application entry points and executable binaries for the project. +Contains the main deployable application modules that compose the complete system. ## files ## arch -Follows a workspace/monorepo pattern where each subdirectory is a distinct deployable application sharing common libraries. +Modular application architecture with separate entry points for different application contexts or deployment targets. ## tags - ## symbols diff --git a/apps/api/.pi-map.index.md b/apps/api/.pi-map.index.md index fb1d36f..52afd12 100644 --- a/apps/api/.pi-map.index.md +++ b/apps/api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api ## role -Backend API server providing project management, git repository operations, and development tool orchestration via Docker containers. +Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md @@ -10,6 +10,12 @@ map: apps/.pi-map.md - apps/api/.pi-lens index: apps/api/.pi-lens/.pi-map.index.md map: apps/api/.pi-lens/.pi-map.md +- apps/api/.pytest_cache + index: apps/api/.pytest_cache/.pi-map.index.md + map: apps/api/.pytest_cache/.pi-map.md +- apps/api/.venv-test + index: apps/api/.venv-test/.pi-map.index.md + map: apps/api/.venv-test/.pi-map.md - apps/api/alembic index: apps/api/alembic/.pi-map.index.md map: apps/api/alembic/.pi-map.md diff --git a/apps/api/.pi-map.md b/apps/api/.pi-map.md index fa9e602..4669240 100644 --- a/apps/api/.pi-map.md +++ b/apps/api/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api index: apps/api/.pi-map.index.md ## role -Backend API server providing project management, git repository operations, and development tool orchestration via Docker containers. +Self-hosted FastAPI backend API that manages projects, git repositories, and development tools via Docker instances. ## files - .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker - Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies @@ -14,7 +14,7 @@ Backend API server providing project management, git repository operations, and - uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages - wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep ## arch -FastAPI-based async Python service with multi-stage Docker deployment, PostgreSQL database with Alembic migrations, Cloudflare tunnel integration, and uv package management. +Multi-stage Dockerized Python application using async FastAPI, PostgreSQL with Alembic migrations, uv package management, and Cloudflare tunneling with external Docker socket access. ## tags docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn ## symbols diff --git a/apps/api/tests/.pi-map.index.md b/apps/api/tests/.pi-map.index.md index 77ee746..77f726f 100644 --- a/apps/api/tests/.pi-map.index.md +++ b/apps/api/tests/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests ## role -Shared testing infrastructure and fixtures for the FastAPI API application +Provides shared test infrastructure and fixtures for the FastAPI API application. ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md diff --git a/apps/api/tests/.pi-map.md b/apps/api/tests/.pi-map.md index 44ba75c..99ddf16 100644 --- a/apps/api/tests/.pi-map.md +++ b/apps/api/tests/.pi-map.md @@ -4,11 +4,11 @@ dir: apps/api/tests index: apps/api/tests/.pi-map.index.md ## role -Shared testing infrastructure and fixtures for the FastAPI API application +Provides shared test infrastructure and fixtures for the FastAPI API application. ## files - conftest.py | Provides shared pytest fixtures for FastAPI testing with async SQLite database, authenticated clients, and test data setup. | exp: func:test_client() → Generator[TestClient, None, None], call:create_async_engine, call:engine.begin, call:conn.run_sync, call:asyncio.run, call:init_db, call:async_sessionmaker, call:patch, call:TestClient, call:app.dependency_overrides.pop, call:engine.dispose, func:init_db(), call:engine.begin, call:conn.run_sync, func:override_get_db_session() → AsyncGenerator[AsyncSession, None], call:async_sessionmaker, func:db_session(test_client) → AsyncGenerator[AsyncSession, None], call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:gen.aclose, call:create_async_engine, call:engine.begin, call:conn.run_sync, call:async_sessionmaker, call:engine.dispose, func:authenticated_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_test_user, call:create_session_cookie, call:test_client.cookies.set, func:create_test_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, func:test_project_and_repo(authenticated_client) → tuple[str, str], call:uuid.uuid4, call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, call:asyncio.run, call:get_user_id, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, call:create_project_and_repo, call:str, raise:RuntimeError, func:get_user_id(), call:Settings, call:authenticated_client.cookies.get, call:decode_session_cookie, call:uuid.UUID, func:create_project_and_repo(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:Project, call:session.add, call:GitRepository, call:session.commit, call:gen.aclose, func:admin_client(test_client) → Generator[TestClient, None, None], call:str, call:uuid.uuid4, call:Settings, call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose, call:asyncio.run, call:create_admin_user, call:create_session_cookie, call:test_client.cookies.set, func:create_admin_user(), call:app.dependency_overrides.get, call:override_fn, call:gen.asend, call:User, call:uuid.UUID, call:session.add, call:session.commit, call:gen.aclose | dep: asyncio, os, typing, unittest.mock, pytest, pytest_asyncio, fastapi.testclient, sqlalchemy.ext.asyncio, src.config, src.models.base, src.main, src.auth.dependencies, uuid, src.auth.session, src.models.user, src.models.project, src.models.git_repository, fastapi, sqlalchemy, src.models, src.auth ## arch -Pytest fixture-based dependency injection pattern with async SQLite test database, authenticated HTTP client factories, and modular test data setup utilities +Pytest fixture-based testing architecture with async SQLite database setup, authenticated HTTP client factories, and test data seeding patterns. ## tags call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, session ## symbols diff --git a/apps/api/tests/integration/.pi-map.index.md b/apps/api/tests/integration/.pi-map.index.md index f33ae17..a493fbd 100644 --- a/apps/api/tests/integration/.pi-map.index.md +++ b/apps/api/tests/integration/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests/integration ## role -Integration and unit test suite for the API backend, covering authentication, REST endpoints, database models, and core services with real database dependencies. +Integration and unit test suite for the FastAPI backend API, covering authentication, CRUD APIs, workspace/git operations, and event/notification systems using real databases and external dependencies. ## parent index: apps/api/tests/.pi-map.index.md map: apps/api/tests/.pi-map.md @@ -24,6 +24,9 @@ map: apps/api/tests/.pi-map.md - test_tool_types_api.py - test_tool_types_api_extended.py - test_users_api.py +- test_workspace_files.py +- test_workspace_git.py +- test_workspace_instances.py - test_workspaces_api.py ## links index: apps/api/tests/integration/.pi-map.index.md diff --git a/apps/api/tests/integration/.pi-map.md b/apps/api/tests/integration/.pi-map.md index 3a2c790..7385404 100644 --- a/apps/api/tests/integration/.pi-map.md +++ b/apps/api/tests/integration/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api/tests/integration index: apps/api/tests/integration/.pi-map.index.md ## role -Integration and unit test suite for the API backend, covering authentication, REST endpoints, database models, and core services with real database dependencies. +Integration and unit test suite for the FastAPI backend API, covering authentication, CRUD APIs, workspace/git operations, and event/notification systems using real databases and external dependencies. ## files - __init__.py | Empty file with no functionality - test_auth_api.py | Integration tests for FastAPI authentication endpoints (login, me, logout) using a real database and session cookies. | exp: func:_prepare_auth_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_login_redirects_to_authentik_authorize_endpoint() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_401_without_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.get, func:test_me_returns_user_with_valid_session() → None, call:_prepare_auth_test_db, call:_insert_test_user, call:_load_app, call:Settings, call:create_session_cookie, call:TestClient, call:client.get, call:response.json, func:test_logout_clears_session_cookie() → None, call:_prepare_auth_test_db, call:_load_app, call:TestClient, call:client.post, call:response.headers.get | dep: uuid, asyncio, importlib, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user, fastapi, src.database, src.api.auth, src.main @@ -21,20 +21,23 @@ Integration and unit test suite for the API backend, covering authentication, RE - test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint covering CRUD operations, authentication, and validation rules | exp: func:_prepare_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:hasattr, call:asyncio.run, call:database_module.engine.dispose, call:importlib.reload, func:_mint_token(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:_insert_user(user_id: str, email) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_insert_tool_type(tool_type_id: str, name: str, display_name: str, compose_template: str, created_by_id) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:ToolType, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_list_tool_types_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.get, func:test_list_tool_types_returns_all_types() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, call:next, func:test_get_tool_type_by_id() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, func:test_get_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, func:test_create_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, call:response.json, func:test_create_tool_type_duplicate_name() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_invalid_yaml() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_services() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_create_tool_type_missing_required_variable() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.post, func:test_update_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, call:response.json, func:test_update_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.put, func:test_delete_tool_type_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_tool_type, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, call:client.get, func:test_delete_tool_type_not_found() → None, call:_prepare_test_db, call:_insert_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.tool_type, src.models.user, importlib, fastapi, src.database, src.api.auth, src.api.tool_types, src.main - test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields including dockerfile templates, compose templates, readiness probes, startup commands, and validation logic. | exp: class:TestToolTypesAPIExtended, method:test_create_tool_type_with_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_readiness_probe(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_invalid_definition_type(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_create_tool_type_dockerfile_without_template(self, authenticated_client: TestClient) → None, call:authenticated_client.post, method:test_update_tool_type_with_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_validate_tool_type_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_invalid_compose(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_validate_tool_type_dockerfile(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_get_tool_type_returns_new_fields(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, method:test_create_tool_type_without_port_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, call:str, method:test_create_tool_type_with_port_mismatch_fails(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_create_tool_type_with_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:response.json, method:test_update_tool_type_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.put, call:response.json, method:test_get_tool_type_returns_startup_command(self, authenticated_client: TestClient) → None, call:authenticated_client.post, call:create_response.json, call:authenticated_client.get, call:response.json, call:str | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient - test_users_api.py | Integration tests for user profile API endpoints including get, update, and avatar upload with authentication and validation | exp: func:_prepare_users_test_db() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:connection.execute, call:text, call:engine.dispose, func:_load_app(), call:importlib.reload, func:_insert_test_user(user_id: str) → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, call:asyncio.run, call:_run, func:_run() → None, call:create_async_engine, call:build_database_url, call:engine.begin, call:connection.run_sync, call:async_sessionmaker, call:session_factory, call:User, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:_create_auth_cookie(user_id: str) → str, call:Settings, call:create_session_cookie, call:datetime.now, call:timedelta, func:test_get_profile_returns_401_without_cookie() → None, call:_prepare_users_test_db, call:_load_app, call:TestClient, call:client.get, func:test_get_profile_returns_user_data() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.get, call:response.json, func:test_update_profile_changes_name_and_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, call:response.json, func:test_update_profile_rejects_empty_name() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_update_profile_rejects_invalid_email() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.put, func:test_upload_avatar_updates_avatar_url() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, call:response.json, call:data["avatar_url"].startswith, func:test_upload_avatar_rejects_invalid_file_type() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO, func:test_upload_avatar_rejects_oversized_file() → None, call:_prepare_users_test_db, call:_insert_test_user, call:_load_app, call:TestClient, call:client.cookies.set, call:_create_auth_cookie, call:client.post, call:io.BytesIO | dep: uuid, datetime, asyncio, io, fastapi.testclient, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.user, importlib, fastapi, src.database, src.api.users, src.main -- test_workspaces_api.py | Integration tests for FastAPI workspace API endpoints covering CRUD, sync, and deletion operations with mocked services. | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models.git_repository, src.models.project, src.models.tool_instance, src.models.tool_type, src.models.workspace, src.services.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy, src.models +- test_workspace_files.py | Integration tests for workspace file endpoints (list, read, write) with path traversal security checks | exp: class:TestListWorkspaceFiles, method:test_list_files(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:os.makedirs, call:os.path.join, call:open, call:f.write, call:authenticated_client.get, call:response.json, method:test_list_files_path_escapes_workspace(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:authenticated_client.get, class:TestGetFileContent, method:test_get_content(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:authenticated_client.get, call:response.json, method:test_get_missing_file(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:authenticated_client.get, class:TestWriteFileContent, method:test_write_file(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:authenticated_client.post, call:os.path.exists, call:os.path.join, method:test_write_file_missing_path(self, authenticated_client: TestClient, test_repo_with_workspace: Workspace), call:authenticated_client.post, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo_with_workspace(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:db_session.commit, call:db_session.refresh | dep: asyncio, os, tempfile, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi.testclient.TestClient, sqlalchemy.ext.asyncio.AsyncSession +- test_workspace_git.py | Integration tests for workspace Git endpoints (status, commit, history, checkout) using real git repositories and FastAPI test client. | exp: class:TestGitStatus, method:test_status_clean(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:authenticated_client.get, call:response.json, method:test_status_modified(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:authenticated_client.get, call:response.json, class:TestGitCommit, method:test_commit(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:authenticated_client.post, call:response.json, method:test_commit_missing_message(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:authenticated_client.post, class:TestGitHistory, method:test_history(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:authenticated_client.get, call:response.json, call:len, class:TestGitCheckout, method:test_checkout(self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace), call:_run_git, call:authenticated_client.post, call:response.json, func:_run_git(*args: str, cwd: str) → None, call:subprocess.run, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo_with_git_workspace(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:_run_git, call:open, call:os.path.join, call:f.write, call:Workspace, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:_run_git, call:open, call:os.path.join, call:f.write, call:Workspace, call:db_session.commit, call:db_session.refresh | dep: asyncio, os, subprocess, tempfile, uuid, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, fastapi.testclient.TestClient, sqlalchemy.ext.asyncio.AsyncSession +- test_workspace_instances.py | Integration tests for workspace instance REST API endpoints (listing and creating tool instances within workspaces) | exp: class:TestListWorkspaceInstances, method:test_list_empty(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:authenticated_client.get, call:response.json, method:test_list_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_workspace_with_tool_type), call:ToolInstance, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_create_instance, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspaceInstance, method:test_create_instance(self, authenticated_client: TestClient, test_workspace_with_tool_type), call:ToolInstance, call:uuid.uuid4, call:datetime.now, call:patch, call:authenticated_client.post, call:str, call:response.json, func:_get_user_id(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_workspace_with_tool_type(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:tempfile.mkdtemp, call:Workspace, call:ToolType, call:db_session.commit, call:db_session.refresh | dep: asyncio, tempfile, uuid, datetime, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.auth.session, src.config, src.api.workspace.workspace_instances +- test_workspaces_api.py | Integration tests for workspace API endpoints including list, create, delete, and sync operations with repository/project scoping | exp: class:TestListWorkspaces, method:test_list_empty(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.get, call:response.json, method:test_list_with_workspaces(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:authenticated_client.get, call:response.json, call:len, class:TestCreateWorkspace, method:test_create_success(self, authenticated_client: TestClient, test_repo: GitRepository), call:Workspace, call:uuid.uuid4, call:patch.object, call:authenticated_client.post, call:response.json, call:mock_create.assert_called_once, method:test_create_missing_name(self, authenticated_client: TestClient, test_repo: GitRepository), call:authenticated_client.post, call:response.json, method:test_create_duplicate_name(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:asyncio.run, call:_commit, call:patch.object, call:Exception, call:authenticated_client.post, class:TestDeleteWorkspace, method:test_delete_without_instances(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, call:response.json, method:test_delete_with_instances_force(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:authenticated_client.delete, class:TestSyncWorkspace, method:test_sync_success(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, method:test_sync_branch_deleted(self, authenticated_client: TestClient, db_session: AsyncSession, test_repo: GitRepository), call:Workspace, call:db_session.add, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_commit_refresh, call:patch.object, call:MagicMock, call:authenticated_client.post, call:response.json, func:_get_user_id_from_client(client: TestClient) → uuid.UUID, call:Settings, call:client.cookies.get, call:decode_session_cookie, call:uuid.UUID, raise:RuntimeError, func:test_repo(db_session: AsyncSession, authenticated_client: TestClient), call:_get_user_id_from_client, call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh, call:asyncio.run, call:_create, func:_create(), call:Project, call:db_session.add, call:db_session.flush, call:GitRepository, call:db_session.commit, call:db_session.refresh | dep: asyncio, uuid, unittest.mock, pytest, fastapi.testclient, sqlalchemy.ext.asyncio, src.models, src.services.shared.workspace_manager, src.auth.session, src.config, fastapi, sqlalchemy ## arch -Test pyramid with integration tests dominating; uses FastAPI TestClient, real SQLAlchemy database sessions, session cookie authentication, pub/sub event bus mocking, and service-layer mocking for external dependencies (git, SSH, workspaces). +Test-driven pyramid with integration tests as primary layer using FastAPI TestClient, real SQLAlchemy databases, actual git repositories, and pub/sub event bus; unit tests limited to isolated git utilities; shared fixtures likely in conftest.py for dependency injection, database setup/teardown, and authenticated client sessions. ## tags -test, call:, call:authenticated, user, call:db, call:create, create, call:response.json +test, call:, call:db, call:authenticated, user, call:response.json, create, call:create ## symbols - TestConfigProfilesAPI - TestGitStatus - TestBranchOperations - TestCommit - TestToolTypesAPIExtended -- TestListWorkspaces -- TestCreateWorkspace -- TestDeleteWorkspace +- TestListWorkspaceFiles +- TestGetFileContent +- TestWriteFileContent ## workflows - change integration behavior read: __init__.py, test_auth_api.py, test_auth_services.py diff --git a/apps/api/tests/integration/test_workspace_files.py b/apps/api/tests/integration/test_workspace_files.py new file mode 100644 index 0000000..e3209d4 --- /dev/null +++ b/apps/api/tests/integration/test_workspace_files.py @@ -0,0 +1,142 @@ +"""Integration tests for workspace file endpoints.""" + +import asyncio +import os +import tempfile +import uuid +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession + +from src.models import GitRepository, Project, Workspace +from src.services.shared.workspace_manager import WorkspaceManager + + +def _get_user_id(client: TestClient) -> uuid.UUID: + from src.auth.session import decode_session_cookie + from src.config import Settings + + settings = Settings() + cookie = client.cookies.get("session") + data = decode_session_cookie(settings=settings, cookie_value=cookie) + if not data: + raise RuntimeError("No session") + return uuid.UUID(data["user_id"]) + + +@pytest.fixture +def test_repo_with_workspace( + db_session: AsyncSession, authenticated_client: TestClient +): + """Create a project, repo, and workspace backed by a temp directory.""" + user_id = _get_user_id(authenticated_client) + + async def _create(): + project = Project(name="File Test Project", owner_id=user_id) + db_session.add(project) + await db_session.flush() + + repo = GitRepository( + name="file-test-repo", + path="/tmp/file-test-repo", + remote_url="https://example.com/repo.git", + project_id=project.id, + owner_id=user_id, + ) + db_session.add(repo) + await db_session.flush() + + tmpdir = tempfile.mkdtemp() + ws = Workspace( + name="dev", + repo_id=repo.id, + user_id=user_id, + branch="main", + path=tmpdir, + ) + db_session.add(ws) + await db_session.commit() + await db_session.refresh(ws) + return ws + + return asyncio.run(_create()) + + +class TestListWorkspaceFiles: + """Tests for GET /workspaces/{id}/files.""" + + def test_list_files( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + ws = test_repo_with_workspace + os.makedirs(os.path.join(ws.path, "src")) + with open(os.path.join(ws.path, "README.md"), "w") as f: + f.write("# Test") + + response = authenticated_client.get(f"/workspaces/{ws.id}/files") + assert response.status_code == 200 + data = response.json() + names = {e["name"] for e in data["entries"]} + assert "src" in names + assert "README.md" in names + + def test_list_files_path_escapes_workspace( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + response = authenticated_client.get( + f"/workspaces/{test_repo_with_workspace.id}/files", params={"path": "../outside"} + ) + assert response.status_code == 400 + + +class TestGetFileContent: + """Tests for GET /workspaces/{id}/files/content.""" + + def test_get_content( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + with open(os.path.join(test_repo_with_workspace.path, "hello.py"), "w") as f: + f.write("print('hello')") + + response = authenticated_client.get( + f"/workspaces/{test_repo_with_workspace.id}/files/content", + params={"path": "hello.py"}, + ) + assert response.status_code == 200 + assert response.json()["content"] == "print('hello')" + + def test_get_missing_file( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + response = authenticated_client.get( + f"/workspaces/{test_repo_with_workspace.id}/files/content", + params={"path": "missing.txt"}, + ) + assert response.status_code == 404 + + +class TestWriteFileContent: + """Tests for POST /workspaces/{id}/files/content.""" + + def test_write_file( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + response = authenticated_client.post( + f"/workspaces/{test_repo_with_workspace.id}/files/content", + json={"path": "nested/file.txt", "content": "content"}, + ) + assert response.status_code == 200 + assert os.path.exists( + os.path.join(test_repo_with_workspace.path, "nested", "file.txt") + ) + + def test_write_file_missing_path( + self, authenticated_client: TestClient, test_repo_with_workspace: Workspace + ): + response = authenticated_client.post( + f"/workspaces/{test_repo_with_workspace.id}/files/content", + json={"content": "content"}, + ) + assert response.status_code == 400 diff --git a/apps/api/tests/integration/test_workspace_git.py b/apps/api/tests/integration/test_workspace_git.py new file mode 100644 index 0000000..4d5b841 --- /dev/null +++ b/apps/api/tests/integration/test_workspace_git.py @@ -0,0 +1,160 @@ +"""Integration tests for workspace git endpoints.""" + +import asyncio +import os +import subprocess +import tempfile +import uuid + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession + +from src.models import GitRepository, Project, Workspace + + +def _run_git(*args: str, cwd: str) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True) + + +def _get_user_id(client: TestClient) -> uuid.UUID: + from src.auth.session import decode_session_cookie + from src.config import Settings + + settings = Settings() + cookie = client.cookies.get("session") + data = decode_session_cookie(settings=settings, cookie_value=cookie) + if not data: + raise RuntimeError("No session") + return uuid.UUID(data["user_id"]) + + +@pytest.fixture +def test_repo_with_git_workspace( + db_session: AsyncSession, authenticated_client: TestClient +): + """Create a workspace backed by a real git repo with commits.""" + user_id = _get_user_id(authenticated_client) + + async def _create(): + project = Project(name="Git Test Project", owner_id=user_id) + db_session.add(project) + await db_session.flush() + + repo = GitRepository( + name="git-test-repo", + path="/tmp/git-test-repo", + remote_url="https://example.com/repo.git", + project_id=project.id, + owner_id=user_id, + ) + db_session.add(repo) + await db_session.flush() + + tmpdir = tempfile.mkdtemp() + _run_git("init", cwd=tmpdir) + _run_git("config", "user.email", "test@test.com", cwd=tmpdir) + _run_git("config", "user.name", "Test User", cwd=tmpdir) + with open(os.path.join(tmpdir, "README.md"), "w") as f: + f.write("# Test\n") + _run_git("add", "README.md", cwd=tmpdir) + _run_git("commit", "-m", "Initial commit", cwd=tmpdir) + + ws = Workspace( + name="dev", + repo_id=repo.id, + user_id=user_id, + branch="main", + path=tmpdir, + ) + db_session.add(ws) + await db_session.commit() + await db_session.refresh(ws) + return ws + + return asyncio.run(_create()) + + +class TestGitStatus: + """Tests for GET /workspaces/{id}/git/status.""" + + def test_status_clean( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + response = authenticated_client.get( + f"/workspaces/{test_repo_with_git_workspace.id}/git/status" + ) + assert response.status_code == 200 + data = response.json() + assert data["branch"] == "main" + assert data["modified"] == [] + + def test_status_modified( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + with open(os.path.join(test_repo_with_git_workspace.path, "README.md"), "a") as f: + f.write("modified\n") + + response = authenticated_client.get( + f"/workspaces/{test_repo_with_git_workspace.id}/git/status" + ) + assert response.status_code == 200 + assert "README.md" in response.json()["modified"] + + +class TestGitCommit: + """Tests for POST /workspaces/{id}/git/commit.""" + + def test_commit( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + with open(os.path.join(test_repo_with_git_workspace.path, "new.py"), "w") as f: + f.write("print('hello')\n") + + response = authenticated_client.post( + f"/workspaces/{test_repo_with_git_workspace.id}/git/commit", + json={"message": "Add new file"}, + ) + assert response.status_code == 200 + assert response.json()["status"] == "committed" + + def test_commit_missing_message( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + response = authenticated_client.post( + f"/workspaces/{test_repo_with_git_workspace.id}/git/commit", + json={}, + ) + assert response.status_code == 400 + + +class TestGitHistory: + """Tests for GET /workspaces/{id}/git/history.""" + + def test_history( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + response = authenticated_client.get( + f"/workspaces/{test_repo_with_git_workspace.id}/git/history" + ) + assert response.status_code == 200 + data = response.json() + assert len(data["commits"]) >= 1 + assert data["commits"][0]["message"] == "Initial commit" + + +class TestGitCheckout: + """Tests for POST /workspaces/{id}/git/checkout.""" + + def test_checkout( + self, authenticated_client: TestClient, test_repo_with_git_workspace: Workspace + ): + _run_git("checkout", "-b", "feature", cwd=test_repo_with_git_workspace.path) + _run_git("checkout", "main", cwd=test_repo_with_git_workspace.path) + + response = authenticated_client.post( + f"/workspaces/{test_repo_with_git_workspace.id}/git/checkout", + json={"branch": "feature"}, + ) + assert response.status_code == 200 + assert response.json()["branch"] == "feature" diff --git a/apps/api/tests/integration/test_workspace_instances.py b/apps/api/tests/integration/test_workspace_instances.py new file mode 100644 index 0000000..6471c3f --- /dev/null +++ b/apps/api/tests/integration/test_workspace_instances.py @@ -0,0 +1,151 @@ +"""Integration tests for workspace instance endpoints.""" + +import asyncio +import tempfile +import uuid +from datetime import datetime +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncSession + +from src.models import GitRepository, Project, ToolInstance, ToolType, Workspace + + +def _get_user_id(client: TestClient) -> uuid.UUID: + from src.auth.session import decode_session_cookie + from src.config import Settings + + settings = Settings() + cookie = client.cookies.get("session") + data = decode_session_cookie(settings=settings, cookie_value=cookie) + if not data: + raise RuntimeError("No session") + return uuid.UUID(data["user_id"]) + + +@pytest.fixture +def test_workspace_with_tool_type( + db_session: AsyncSession, authenticated_client: TestClient +): + """Create a project, repo, workspace, and tool type.""" + user_id = _get_user_id(authenticated_client) + + async def _create(): + project = Project(name="Instance Test Project", owner_id=user_id) + db_session.add(project) + await db_session.flush() + + repo = GitRepository( + name="instance-test-repo", + path="/tmp/instance-test-repo", + remote_url="https://example.com/repo.git", + project_id=project.id, + owner_id=user_id, + ) + db_session.add(repo) + await db_session.flush() + + tmpdir = tempfile.mkdtemp() + ws = Workspace( + name="dev", + repo_id=repo.id, + user_id=user_id, + branch="main", + path=tmpdir, + ) + db_session.add(ws) + await db_session.flush() + + tool_type = ToolType( + name="test-tool", + display_name="Test Tool", + default_port=8080, + category="dev", + ) + db_session.add(tool_type) + await db_session.commit() + await db_session.refresh(ws) + await db_session.refresh(tool_type) + return ws, tool_type, project + + return asyncio.run(_create()) + + +class TestListWorkspaceInstances: + """Tests for GET /workspaces/{id}/instances.""" + + def test_list_empty( + self, authenticated_client: TestClient, test_workspace_with_tool_type + ): + ws, _, _ = test_workspace_with_tool_type + response = authenticated_client.get(f"/workspaces/{ws.id}/instances") + assert response.status_code == 200 + assert response.json() == [] + + def test_list_instances( + self, + authenticated_client: TestClient, + db_session: AsyncSession, + test_workspace_with_tool_type, + ): + ws, tool_type, project = test_workspace_with_tool_type + + async def _create_instance(): + instance = ToolInstance( + name="test-instance", + display_name="Test Instance", + tool_type_id=tool_type.id, + repository_id=ws.repo_id, + project_id=project.id, + owner_id=ws.user_id, + workspace_id=ws.id, + status="running", + ) + db_session.add(instance) + await db_session.commit() + + asyncio.run(_create_instance()) + + response = authenticated_client.get(f"/workspaces/{ws.id}/instances") + assert response.status_code == 200 + data = response.json() + assert len(data) == 1 + assert data[0]["display_name"] == "Test Instance" + + +class TestCreateWorkspaceInstance: + """Tests for POST /workspaces/{id}/instances.""" + + def test_create_instance( + self, + authenticated_client: TestClient, + test_workspace_with_tool_type, + ): + ws, tool_type, project = test_workspace_with_tool_type + mock_instance = ToolInstance( + id=uuid.uuid4(), + name="mock-instance", + display_name="Mock Instance", + tool_type_id=tool_type.id, + repository_id=ws.repo_id, + project_id=project.id, + owner_id=ws.user_id, + workspace_id=ws.id, + status="pending", + created_at=datetime.now(), + ) + + with patch( + "src.api.workspace.workspace_instances.create_tool_instance", + return_value=mock_instance, + ): + response = authenticated_client.post( + f"/workspaces/{ws.id}/instances", + json={"tool_type_id": str(tool_type.id), "display_name": "New Instance"}, + ) + assert response.status_code == 201 + data = response.json() + assert data["display_name"] == "Mock Instance" + assert data["status"] == "pending" diff --git a/apps/api/tests/integration/test_workspaces_api.py b/apps/api/tests/integration/test_workspaces_api.py index cb7f4d5..07e8eec 100644 --- a/apps/api/tests/integration/test_workspaces_api.py +++ b/apps/api/tests/integration/test_workspaces_api.py @@ -8,12 +8,14 @@ import pytest from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import AsyncSession -from src.models.git_repository import GitRepository -from src.models.project import Project -from src.models.tool_instance import ToolInstance -from src.models.tool_type import ToolType -from src.models.workspace import Workspace -from src.services.workspace_manager import WorkspaceManager +from src.models import ( + GitRepository, + Project, + ToolInstance, + ToolType, + Workspace, +) +from src.services.shared.workspace_manager import WorkspaceManager def _get_user_id_from_client(client: TestClient) -> uuid.UUID: diff --git a/apps/api/tests/unit/.pi-map.index.md b/apps/api/tests/unit/.pi-map.index.md index 0b61dff..de4a266 100644 --- a/apps/api/tests/unit/.pi-map.index.md +++ b/apps/api/tests/unit/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests/unit ## role -Unit test suite for the API backend, covering configuration, Docker services, git operations, file handling, health monitoring, notifications, permissions, and lifecycle management. +Unit test suite for the API backend, covering configuration, Docker operations, Git services, file handling, health monitoring, notifications, and SSH key management. ## parent index: apps/api/tests/.pi-map.index.md map: apps/api/tests/.pi-map.md @@ -18,6 +18,7 @@ map: apps/api/tests/.pi-map.md - test_file_service.py - test_git_mount_resolution.py - test_git_mounts.py +- test_git_operations.py - test_git_repository_clone_preflight.py - test_git_repository_working_clones.py - test_git_service.py @@ -32,9 +33,7 @@ map: apps/api/tests/.pi-map.md - test_notifications_api_routes.py - test_permission_fixer.py - test_readiness_probe.py -- test_session_branch_selection.py - test_ssh_keys.py -- test_tool_instances_legacy.py ## links index: apps/api/tests/unit/.pi-map.index.md map: apps/api/tests/unit/.pi-map.md diff --git a/apps/api/tests/unit/.pi-map.md b/apps/api/tests/unit/.pi-map.md index d2e3561..31db71b 100644 --- a/apps/api/tests/unit/.pi-map.md +++ b/apps/api/tests/unit/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api/tests/unit index: apps/api/tests/unit/.pi-map.index.md ## role -Unit test suite for the API backend, covering configuration, Docker services, git operations, file handling, health monitoring, notifications, permissions, and lifecycle management. +Unit test suite for the API backend, covering configuration, Docker operations, Git services, file handling, health monitoring, notifications, and SSH key management. ## files - __init__.py | Empty file with no functionality - test_config.py | Unit tests for application configuration settings, database URL construction, and environment-specific behavior | exp: func:test_settings_default_database_url_uses_asyncpg() → None, call:Settings, func:test_build_database_url_uses_explicit_values() → None, call:build_database_url, func:test_settings_prefers_explicit_database_url_env(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_auth_settings_have_secure_defaults() → None, call:Settings, call:settings.resolved_authentik_authorize_url.endswith, call:settings.resolved_authentik_token_url.endswith, call:settings.resolved_authentik_jwks_url.endswith, func:test_cookie_policy_is_strict_in_production(monkeypatch) → None, call:monkeypatch.setenv, call:Settings, func:test_cookie_policy_is_relaxed_for_local_dev(monkeypatch) → None, call:monkeypatch.setenv, call:Settings | dep: pytest, src.config, src.database @@ -12,9 +12,10 @@ Unit test suite for the API backend, covering configuration, Docker services, gi - test_docker_build.py | Unit tests for a Docker image build service that validates successful builds, failures, command structure, file writing, context handling, path traversal prevention, timeouts, and exceptions. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.docker_build - test_docker_service.py | Unit tests for Docker service utility functions including container ID/name retrieval and volume sorting by mount specificity. | exp: class:TestGetContainerId, class:TestGetContainerName, class:TestSortVolumesBySpecificity, method:test_parent_before_child(self) → None, call:sort_volumes_by_specificity, method:test_stable_sort_for_equal_depth(self) → None, call:sort_volumes_by_specificity, method:test_with_type_suffix(self) → None, call:sort_volumes_by_specificity, method:test_empty_list(self) → None, call:sort_volumes_by_specificity, method:test_single_volume(self) → None, call:sort_volumes_by_specificity, method:test_duplicate_target_warning(self, caplog) → None, call:caplog.at_level, call:sort_volumes_by_specificity | dep: unittest.mock, logging, src.services.docker, subprocess - test_event_bus.py | Unit tests for InstanceEventBus verifying publish/subscribe behavior, exception isolation, unsubscribe functionality, and async callback support | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:sample_payload() → InstanceEventPayload, call:str, call:uuid.uuid4, func:test_publish_delivers_to_all_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, call:len, func:callback_1(payload: InstanceEventPayload) → None, call:received.append, func:callback_2(payload: InstanceEventPayload) → None, call:received.append, func:callback_3(payload: InstanceEventPayload) → None, call:received.append, func:test_subscriber_exception_isolation(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.publish, raise:RuntimeError, func:bad_callback(_payload: InstanceEventPayload) → None, raise:RuntimeError, func:good_callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_unsubscribe_removes_callback(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:unsubscribe, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append, func:test_publish_to_empty_subscriber_list(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:event_bus.publish, func:test_async_subscriber_supported(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, call:event_bus.subscribe, call:event_bus.publish, func:async_callback(_payload: InstanceEventPayload) → None, call:asyncio.sleep, call:received.append, func:test_unsubscribe_all_clears_subscribers(event_bus: InstanceEventBus, sample_payload: InstanceEventPayload) → None, call:received.append, call:event_bus.subscribe, call:event_bus.unsubscribe_all, call:event_bus.publish, func:callback(_payload: InstanceEventPayload) → None, call:received.append | dep: asyncio, uuid, typing, pytest, src.services.event_bus -- test_file_service.py | Unit tests for FileService validating directory listing, file read/write, binary rejection, and path traversal prevention. | exp: class:TestFileService, method:test_list_directory_empty(self, temp_workspace: Workspace), call:FileService, call:service.list_directory, method:test_list_directory_with_files(self, temp_workspace: Workspace), call:os.makedirs, call:os.path.join, call:open, call:f.write, call:FileService, call:service.list_directory, call:len, method:test_read_file(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:service.read_file, method:test_read_binary_file_rejected(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:pytest.raises, call:service.read_file, method:test_write_file(self, temp_workspace: Workspace), call:FileService, call:service.write_file, call:os.path.exists, call:os.path.join, call:open, call:f.read, method:test_path_escapes_workspace(self, temp_workspace: Workspace), call:FileService, call:pytest.raises, call:service.list_directory, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:Workspace | dep: os, tempfile, pytest, src.models.workspace, src.services.file_service +- test_file_service.py | Unit tests for FileService validating directory listing, file read/write, binary rejection, and path traversal prevention. | exp: class:TestFileService, method:test_list_directory_empty(self, temp_workspace: Workspace), call:FileService, call:service.list_directory, method:test_list_directory_with_files(self, temp_workspace: Workspace), call:os.makedirs, call:os.path.join, call:open, call:f.write, call:FileService, call:service.list_directory, call:len, method:test_read_file(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:service.read_file, method:test_read_binary_file_rejected(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:FileService, call:pytest.raises, call:service.read_file, method:test_write_file(self, temp_workspace: Workspace), call:FileService, call:service.write_file, call:os.path.exists, call:os.path.join, call:open, call:f.read, method:test_path_escapes_workspace(self, temp_workspace: Workspace), call:FileService, call:pytest.raises, call:service.list_directory, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:Workspace | dep: os, tempfile, pytest, src.models, src.services.shared.file_service, src.models.Workspace, src.services.shared.file_service.FileService - test_git_mount_resolution.py | Unit tests for git mount resolution functions including glob expansion, branch checkout, and mount validation in tool instances. | exp: class:TestExpandGlobSource, method:test_no_glob_single_file(self, tmp_path: Path) → None, call:test_file.write_text, call:_expand_glob_source, call:str, call:len, method:test_no_glob_missing_file(self, tmp_path: Path) → None, call:_expand_glob_source, call:str, call:len, method:test_glob_pattern(self, tmp_path: Path) → None, call:(tmp_path / "file1.txt").write_text, call:(tmp_path / "file2.txt").write_text, call:(tmp_path / "other.py").write_text, call:_expand_glob_source, call:str, call:len, call:all, call:f.endswith, method:test_glob_recursive(self, tmp_path: Path) → None, call:subdir.mkdir, call:(subdir / "nested.txt").write_text, call:_expand_glob_source, call:str, call:len, method:test_glob_limit_enforced(self, tmp_path: Path) → None, call:range, call:(tmp_path / f"file{i}.txt").write_text, call:_expand_glob_source, call:str, call:len, method:test_glob_escapes_repo(self, tmp_path: Path) → None, call:other_dir.mkdir, call:(other_dir / "outside.txt").write_text, call:_expand_glob_source, call:str, call:all, call:r.startswith, class:TestCheckoutBranch, method:test_checkout_existing_branch(self, tmp_path: Path) → None, call:os.system, call:(tmp_path / "file.txt").write_text, call:_checkout_branch, call:str, call:os.popen(f"cd {tmp_path} && git branch --show-current").read().strip, method:test_checkout_nonexistent_branch(self, tmp_path: Path) → None, call:os.system, call:(tmp_path / "file.txt").write_text, call:_checkout_branch, call:str, class:TestResolveSingleGitMount | dep: os, pathlib, pytest, src.api.tool_instances, pathlib.Path - test_git_mounts.py | Unit tests for git mount resolution functions that handle multi-mapping support, normalization, glob expansion, and Docker volume bind mounting. | exp: class:TestNormalizeGitMount, method:test_legacy_to_mappings(self) → None, call:_normalize_git_mount, method:test_already_mappings(self) → None, call:_normalize_git_mount, method:test_missing_target_path_no_mappings(self) → None, call:_normalize_git_mount, class:TestResolveGitMountMappings, method:test_single_mapping(self) → None, call:tempfile.TemporaryDirectory, call:os.makedirs, call:os.path.join, call:_resolve_git_mount_mappings, call:len, method:test_multiple_mappings(self) → None, call:tempfile.TemporaryDirectory, call:os.makedirs, call:os.path.join, call:_resolve_git_mount_mappings, call:len, method:test_relative_target_path(self) → None, call:tempfile.TemporaryDirectory, call:os.makedirs, call:os.path.join, call:_resolve_git_mount_mappings, call:len, method:test_glob_expansion(self) → None, call:tempfile.TemporaryDirectory, call:os.makedirs, call:os.path.join, call:_resolve_git_mount_mappings, call:len, method:test_missing_target_path_skipped(self) → None, call:tempfile.TemporaryDirectory, call:_resolve_git_mount_mappings, call:len, method:test_no_working_directory_for_relative_target(self) → None, call:tempfile.TemporaryDirectory, call:os.makedirs, call:os.path.join, call:_resolve_git_mount_mappings, call:len, class:TestResolveSingleGitMount, class:TestExpandGlobSource, method:test_no_glob(self) → None, call:tempfile.TemporaryDirectory, call:os.path.join, call:open(path, "w").close, call:_expand_glob_source, method:test_no_glob_missing(self) → None, call:tempfile.TemporaryDirectory, call:os.path.join, call:_expand_glob_source, method:test_glob_pattern(self) → None, call:tempfile.TemporaryDirectory, call:open(os.path.join(tmp, "a.txt"), "w").close, call:os.path.join, call:open(os.path.join(tmp, "b.txt"), "w").close, call:_expand_glob_source, call:len | dep: os, tempfile, unittest.mock, pytest, src.api.tool_instances +- test_git_operations.py | Unit tests for GitOperations class covering git status, commit, history, and branch operations | exp: class:TestGitOperationsStatus, method:test_status_clean(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.status, method:test_status_modified(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, method:test_status_untracked(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.status, class:TestGitOperationsCommit, method:test_commit_stages_and_commits(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.status, call:git.history, method:test_commit_fails_without_changes(self, temp_workspace: Workspace), call:GitOperations, call:pytest.raises, call:asyncio.run, call:git.commit, class:TestGitOperationsHistory, method:test_history_returns_commits(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.history, call:len, method:test_history_filters_by_path(self, temp_workspace: Workspace), call:open, call:os.path.join, call:f.write, call:GitOperations, call:asyncio.run, call:git.commit, call:git.history, call:len, class:TestGitOperationsBranches, method:test_branches_lists_main(self, temp_workspace: Workspace), call:GitOperations, call:asyncio.run, call:git.branches, method:test_checkout_switches_branch(self, temp_workspace: Workspace), call:_run_git, call:GitOperations, call:asyncio.run, call:git.checkout, call:git.status, func:_run_git(*args: str, cwd: str) → None, call:subprocess.run, func:temp_workspace(), call:tempfile.TemporaryDirectory, call:_run_git, call:os.path.join, call:open, call:f.write, call:Workspace | dep: asyncio, os, subprocess, tempfile, pytest, src.models, src.services.git.git_operations, src.models.Workspace, src.services.git.git_operations.GitOperations - test_git_repository_clone_preflight.py | Unit tests for git repository clone URL building and preflight accessibility checks | exp: func:test_build_provider_clone_url_uses_fixed_host() → None, call:_build_provider_clone_url, func:test_preflight_remote_repository_allows_accessible_repo() → None, call:Mock, call:patch, call:_preflight_remote_repository, call:run_mock.assert_called_once, func:test_preflight_remote_repository_rejects_missing_repo() → None, call:Mock, call:patch, call:pytest.raises, call:_preflight_remote_repository | dep: unittest.mock, pytest, fastapi, src.api.git_repositories, subprocess - test_git_repository_working_clones.py | Tests git repository cloning and initialization utilities with fallback strategies for different git versions | exp: func:test_clone_working_repository_uses_normal_clone() → None, call:Mock, call:patch, call:_clone_working_repository, call:run_mock.assert_called_once, func:test_clone_working_repository_raises_on_failure() → None, call:Mock, call:patch, call:pytest.raises, call:_clone_working_repository, func:test_init_working_repository_prefers_init_b() → None, call:Mock, call:patch, call:_init_working_repository, func:test_init_working_repository_falls_back_to_symbolic_ref() → None, call:Mock, call:patch, call:_init_working_repository, func:test_create_branch_uses_orphan_checkout_when_head_is_unborn() → None, call:patch, call:create_branch, raise:RuntimeError, func:mock_run(repo_path: str, *args: str) → str, raise:RuntimeError | dep: unittest.mock, pytest, fastapi, src.api.git_repositories, src.utils.git_control, subprocess - test_git_service.py | Unit tests for GitService class covering clone, fetch, pull, and branch_exists_remotely operations with mocked subprocess calls. | exp: class:TestGitServiceClone, class:TestGitServiceFetch, class:TestGitServicePull, class:TestGitServiceBranchExistsRemotely, method:test_branch_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, call:mock_run.assert_called_once_with, method:test_branch_not_exists(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely, method:test_ls_remote_fails(self), call:MagicMock, call:patch, call:GitService.branch_exists_remotely | dep: asyncio, unittest.mock, pytest, src.services.git_service @@ -29,13 +30,11 @@ Unit test suite for the API backend, covering configuration, Docker services, gi - test_notifications_api_routes.py | Tests that FastAPI notification routes are declared in correct order so bulk DELETE /notifications matches before parameterized DELETE /notifications/{id} | exp: func:test_delete_notifications_route_order() → None, call:FastAPI, call:app.include_router, call:TestClient, call:client.delete | dep: fastapi, fastapi.testclient, src.api.notifications - test_permission_fixer.py | Unit tests for a Docker container permission fixer service that applies chown/chmod/file_mode to mounts and SSH directories. | exp: class:TestApplyMountPermissions, class:TestRunInContainer, class:TestApplySshPermissions, class:TestCheckRootUserAvailable | dep: unittest.mock, pytest, src.services.permission_fixer, subprocess - test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic and timeout handling. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.readiness_probe, subprocess -- test_session_branch_selection.py | Tests session creation with branch selection and new branch creation via git operations and API request validation. | exp: class:TestCreateInstanceRequest, method:test_default_values(self), call:CreateInstanceRequest, method:test_clone_mode_with_branch(self), call:CreateInstanceRequest, method:test_new_branch_field(self), call:CreateInstanceRequest, class:TestBranchCreationInClone, method:test_create_local_branch_success(self), call:tempfile.TemporaryDirectory, call:subprocess.run, call:os.path.join, call:open, call:f.write, call:branches_result.stdout.strip, method:test_create_local_branch_invalid_name(self), call:tempfile.TemporaryDirectory, call:subprocess.run, call:os.path.join, call:open, call:f.write, class:TestCreateInstanceAPI, method:test_create_instance_request_validation(self), call:CreateInstanceRequest, method:test_create_instance_with_new_branch_sets_instance_branch(self) | dep: os, subprocess, tempfile, src.api.tool_instances, src.api.tool_instances.CreateInstanceRequest - test_ssh_keys.py | Unit tests for SSH key preparation functionality that decrypts encrypted private keys and writes SSH key files with proper permissions and optional ownership. | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.ssh_keys -- test_tool_instances_legacy.py | Unit tests verifying legacy tool instance fallback paths (dockerfile, compose, legacy definition types) continue using original startup flow after manifest-based flow introduction | exp: class:TestCreateInstanceDockerfileLegacy, class:TestCreateInstanceComposeLegacy, class:TestCreateInstanceManifestNotCalledForLegacy, class:TestStartInstanceLegacyFallback, class:TestStartInstanceSshPermissions, class:TestStartInstanceManifestBranch, func:fake_user_id() → uuid.UUID, call:uuid.uuid4, func:fake_project_id() → uuid.UUID, call:uuid.uuid4, func:fake_repo_id() → uuid.UUID, call:uuid.uuid4, func:fake_tool_type_id() → uuid.UUID, call:uuid.uuid4, func:fake_instance_id() → uuid.UUID, call:uuid.uuid4, func:mock_session(fake_user_id, fake_project_id, fake_repo_id, fake_tool_type_id), call:AsyncMock, call:User, call:MagicMock, call:GitRepository, call:getattr, call:datetime.now, func:_get(model, pk), func:_add(instance), call:getattr, call:datetime.now | dep: os, uuid, datetime, unittest.mock, pytest, src.api.tool_instances, src.models.git_repository, src.models.tool_instance, src.models.tool_type, src.models.ssh_key, src.models.user, fastapi ## arch -Pytest-based unit testing with heavy mocking of external dependencies (Docker, subprocess, git, filesystem), organized by service/component under test, using parameterized tests and fixture-based setup for isolated component verification. +pytest-based unit testing with heavy mocking of external dependencies (Docker, Git subprocesses, databases), organized by service/component under test, using parameterized tests and fixture-based setup for isolated component verification. ## tags -test, url, call:, git, call:notification, instance, mount, call:len +test, call:, url, git, call:notification, mount, call:len, call:db ## symbols - TestMergeFunctions - TestResolveProfile diff --git a/apps/api/tests/unit/test_file_service.py b/apps/api/tests/unit/test_file_service.py index c799cc4..2bbd811 100644 --- a/apps/api/tests/unit/test_file_service.py +++ b/apps/api/tests/unit/test_file_service.py @@ -5,8 +5,8 @@ import tempfile import pytest -from src.models.workspace import Workspace -from src.services.file_service import FileService +from src.models import Workspace +from src.services.shared.file_service import FileService @pytest.fixture diff --git a/apps/api/tests/unit/test_git_operations.py b/apps/api/tests/unit/test_git_operations.py new file mode 100644 index 0000000..aba0f2a --- /dev/null +++ b/apps/api/tests/unit/test_git_operations.py @@ -0,0 +1,133 @@ +"""Unit tests for GitOperations.""" + +import asyncio +import os +import subprocess +import tempfile + +import pytest + +from src.models import Workspace +from src.services.git.git_operations import GitOperations + + +def _run_git(*args: str, cwd: str) -> None: + subprocess.run(["git", *args], cwd=cwd, check=True, capture_output=True) + + +@pytest.fixture +def temp_workspace(): + """Create a temporary git workspace.""" + with tempfile.TemporaryDirectory() as tmpdir: + _run_git("init", cwd=tmpdir) + _run_git("config", "user.email", "test@test.com", cwd=tmpdir) + _run_git("config", "user.name", "Test User", cwd=tmpdir) + + readme = os.path.join(tmpdir, "README.md") + with open(readme, "w") as f: + f.write("# Test\n") + _run_git("add", "README.md", cwd=tmpdir) + _run_git("commit", "-m", "Initial commit", cwd=tmpdir) + + ws = Workspace( + id="00000000-0000-0000-0000-000000000001", + name="test-ws", + repo_id="00000000-0000-0000-0000-000000000002", + user_id="00000000-0000-0000-0000-000000000003", + branch="main", + path=tmpdir, + ) + yield ws + + +class TestGitOperationsStatus: + """Tests for GitOperations.status.""" + + def test_status_clean(self, temp_workspace: Workspace): + git = GitOperations(temp_workspace) + status = asyncio.run(git.status()) + assert status.branch == "main" + assert status.modified == [] + assert status.added == [] + assert status.deleted == [] + assert status.untracked == [] + + def test_status_modified(self, temp_workspace: Workspace): + with open(os.path.join(temp_workspace.path, "README.md"), "a") as f: + f.write("modified\n") + + git = GitOperations(temp_workspace) + status = asyncio.run(git.status()) + assert "README.md" in status.modified + + def test_status_untracked(self, temp_workspace: Workspace): + with open(os.path.join(temp_workspace.path, "new.py"), "w") as f: + f.write("print('hello')\n") + + git = GitOperations(temp_workspace) + status = asyncio.run(git.status()) + assert "new.py" in status.untracked + + +class TestGitOperationsCommit: + """Tests for GitOperations.commit.""" + + def test_commit_stages_and_commits(self, temp_workspace: Workspace): + with open(os.path.join(temp_workspace.path, "new.py"), "w") as f: + f.write("print('hello')\n") + + git = GitOperations(temp_workspace) + asyncio.run(git.commit("Add new file")) + + status = asyncio.run(git.status()) + assert "new.py" not in status.untracked + assert "new.py" not in status.added + + history = asyncio.run(git.history()) + assert history[0].message == "Add new file" + + def test_commit_fails_without_changes(self, temp_workspace: Workspace): + git = GitOperations(temp_workspace) + with pytest.raises(RuntimeError): + asyncio.run(git.commit("Nothing to commit")) + + +class TestGitOperationsHistory: + """Tests for GitOperations.history.""" + + def test_history_returns_commits(self, temp_workspace: Workspace): + git = GitOperations(temp_workspace) + history = asyncio.run(git.history()) + assert len(history) >= 1 + assert history[0].message == "Initial commit" + + def test_history_filters_by_path(self, temp_workspace: Workspace): + with open(os.path.join(temp_workspace.path, "new.py"), "w") as f: + f.write("print('hello')\n") + + git = GitOperations(temp_workspace) + asyncio.run(git.commit("Add new file")) + + history = asyncio.run(git.history(path="new.py")) + assert len(history) == 1 + assert history[0].message == "Add new file" + + +class TestGitOperationsBranches: + """Tests for GitOperations.branches.""" + + def test_branches_lists_main(self, temp_workspace: Workspace): + git = GitOperations(temp_workspace) + branches, current = asyncio.run(git.branches()) + assert "main" in branches + assert current == "main" + + def test_checkout_switches_branch(self, temp_workspace: Workspace): + _run_git("checkout", "-b", "feature", cwd=temp_workspace.path) + _run_git("checkout", "main", cwd=temp_workspace.path) + + git = GitOperations(temp_workspace) + asyncio.run(git.checkout("feature")) + + status = asyncio.run(git.status()) + assert status.branch == "feature" diff --git a/apps/web/.pi-map.index.md b/apps/web/.pi-map.index.md index 44a1bb2..5595ada 100644 --- a/apps/web/.pi-map.index.md +++ b/apps/web/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web ## role -Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. +Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md diff --git a/apps/web/.pi-map.md b/apps/web/.pi-map.md index 5abeae7..8ecfdd8 100644 --- a/apps/web/.pi-map.md +++ b/apps/web/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/web index: apps/web/.pi-map.index.md ## role -Frontend web application providing a React-based UI with code editing, terminal, and routing capabilities for the "headquarter" project. +Frontend web application for a React-based developer workspace with code editing, terminal, and routing capabilities. ## files - .env.example | Template file defining example environment variables for frontend API and application URL configuration - .eslintrc.cjs | Configures ESLint for a TypeScript browser project with modern ECMAScript module support | dep: @typescript-eslint/parser, @typescript-eslint/eslint-plugin, eslint @@ -16,7 +16,7 @@ Frontend web application providing a React-based UI with code editing, terminal, - tsconfig.json | TypeScript configuration file for a React project using Vite with modern ES2020 target and bundler module resolution | dep: typescript, react, vite - vite.config.ts | Configures Vite build tool for a React project with custom dev server port and Vitest test settings. | dep: vite, @vitejs/plugin-react ## arch -Modern React SPA built with Vite and TypeScript, containerized via multi-stage Docker with nginx serving, featuring client-side routing, optimized static asset delivery, and development tooling (ESLint, Vitest). +Modern React SPA built with Vite/TypeScript, served by nginx in a Docker container, using ES modules with Vitest for testing and ESLint for code quality. ## tags react, eslint, vite, typescript, dom, application, nginx, web ## symbols diff --git a/apps/web/src/.pi-map.index.md b/apps/web/src/.pi-map.index.md index c175698..a271abc 100644 --- a/apps/web/src/.pi-map.index.md +++ b/apps/web/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src ## role -Frontend web application entry point and core infrastructure for a React single-page application with authentication, routing, and domain type definitions. +Frontend web application entry point and core infrastructure for a React-based project management or development platform. ## parent index: apps/web/.pi-map.index.md map: apps/web/.pi-map.md diff --git a/apps/web/src/.pi-map.md b/apps/web/src/.pi-map.md index 115c146..2e1753a 100644 --- a/apps/web/src/.pi-map.md +++ b/apps/web/src/.pi-map.md @@ -4,15 +4,15 @@ dir: apps/web/src index: apps/web/src/.pi-map.index.md ## role -Frontend web application entry point and core infrastructure for a React single-page application with authentication, routing, and domain type definitions. +Frontend web application entry point and core infrastructure for a React-based project management or development platform. ## files -- main.tsx | Entry point that bootstraps a React SPA with routing, authentication, and session management context providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/repo-workspace.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom, ./styles/* +- main.tsx | Bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom - router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage - types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos ## arch -Layered React SPA architecture using context providers for cross-cutting concerns (auth/session), declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain models. +Layered React SPA architecture with provider-based dependency injection, declarative routing with nested layouts and route guards, and centralized TypeScript type definitions for domain entities. ## tags -pages, styles, css, router, react, session, dom, workspace +pages, styles, css, router, react, session, dom, project ## symbols - AppRouter - SessionUser diff --git a/apps/web/src/components/.pi-map.index.md b/apps/web/src/components/.pi-map.index.md index cb8226a..98692b0 100644 --- a/apps/web/src/components/.pi-map.index.md +++ b/apps/web/src/components/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src/components ## role -Provides foundational, reusable UI components and utilities for the web application, including layout shell, data states, icon system, code display, routing guards, and toast notification rules. +Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application. ## parent index: apps/web/src/.pi-map.index.md map: apps/web/src/.pi-map.md diff --git a/apps/web/src/components/.pi-map.md b/apps/web/src/components/.pi-map.md index 626623b..b5ae990 100644 --- a/apps/web/src/components/.pi-map.md +++ b/apps/web/src/components/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/web/src/components index: apps/web/src/components/.pi-map.index.md ## role -Provides foundational, reusable UI components and utilities for the web application, including layout shell, data states, icon system, code display, routing guards, and toast notification rules. +Provides reusable, foundational React UI components for layout, authentication, data display, icons, code editing, and notifications across the web application. ## files - app-shell.tsx | Renders the main application shell layout with navigation, session management, and responsive mobile/desktop views for a React Router-based app. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ../state/session-operations, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./features/session/session-progress-panel, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons - code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language @@ -16,7 +16,7 @@ Provides foundational, reusable UI components and utilities for the web applicat - toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events - toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type ## arch -Follows a component-based React architecture with separation of concerns between presentational components (icon, data-states, code-editor), layout/app-shell orchestration, auth-guarded routing (protected-route), and domain-specific utility modules (toast-rules) with colocated unit tests. +Modular component architecture with separation of concerns—each component handles a single responsibility, with dedicated test files alongside implementation, and higher-level components (app-shell, protected-route) composing lower-level primitives (icon, data-states, toast-rules). ## tags toast, state, react, icon, code, event, editor, protected ## symbols diff --git a/apps/web/src/components/features/.pi-map.index.md b/apps/web/src/components/features/.pi-map.index.md index 054480e..9e5e30e 100644 --- a/apps/web/src/components/features/.pi-map.index.md +++ b/apps/web/src/components/features/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/web/src/components/features ## role -Contains specialized UI components for major feature areas of the web application, organizing components by business domain rather than by atomic design level. +Contains reusable React components that implement specific user-facing functionality and business logic features across the web application. ## parent index: apps/web/src/components/.pi-map.index.md map: apps/web/src/components/.pi-map.md diff --git a/apps/web/src/components/features/.pi-map.md b/apps/web/src/components/features/.pi-map.md index 4704bae..42d5214 100644 --- a/apps/web/src/components/features/.pi-map.md +++ b/apps/web/src/components/features/.pi-map.md @@ -4,10 +4,10 @@ dir: apps/web/src/components/features index: apps/web/src/components/features/.pi-map.index.md ## role -Contains specialized UI components for major feature areas of the web application, organizing components by business domain rather than by atomic design level. +Contains reusable React components that implement specific user-facing functionality and business logic features across the web application. ## files ## arch -Feature-based colocation pattern where components are grouped by product functionality (e.g., checkout, dashboard, settings) rather than by component type, typically combining multiple atomic components with domain-specific logic and data fetching. +Feature-based component organization with domain-specific grouping, likely using composition patterns and co-located feature logic (hooks, utils, sub-components) following a modular frontend architecture. ## tags - ## symbols diff --git a/apps/web/src/components/features/git/commit-panel.tsx b/apps/web/src/components/features/git/commit-panel.tsx deleted file mode 100644 index 5737294..0000000 --- a/apps/web/src/components/features/git/commit-panel.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useState } from "react"; - -import { commitChanges } from "../../../api/git-repositories"; - -interface CommitPanelProps { - projectId: string; - repoId: string; - modified: string[]; - added: string[]; - deleted: string[]; - untracked: string[]; - onCommit: () => void; -} - -export const CommitPanel = ({ - projectId, - repoId, - modified, - added, - deleted, - untracked, - onCommit, -}: CommitPanelProps) => { - const [message, setMessage] = useState(""); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const allFiles = [...modified, ...added, ...deleted, ...untracked]; - const hasChanges = allFiles.length > 0; - - const handleCommit = async () => { - if (!message.trim()) { - setError("Please enter a commit message"); - return; - } - setLoading(true); - setError(null); - try { - await commitChanges(projectId, repoId, message); - setMessage(""); - onCommit(); - } catch { - setError("Commit failed. Please try again."); - } finally { - setLoading(false); - } - }; - - if (!hasChanges) return null; - - return ( -
-

Changes

- -
- {modified.map((file) => ( -
- M - {file} -
- ))} - {added.map((file) => ( -
- A - {file} -
- ))} - {deleted.map((file) => ( -
- D - {file} -
- ))} - {untracked.map((file) => ( -
- ? - {file} -
- ))} -
- -
-