feat: implement configurable tool container home directory
- Add ToolType.home_directory column with default /home/user
- Add Alembic migration to add column, set existing rows, and rewrite
/workspace to /home/user/{{WORKSPACE_NAME}} in legacy templates
- Add merge migration fc8f1a20cbf6 to resolve Alembic multiple heads
- Update manifest compiler to honor manifest.home_directory for HOME,
WORKDIR, /workspace symlink, and default repo mount target
- Update legacy dockerfile/compose instance generation to use
tool_type.home_directory
- Thread resolved home_dir through config profile and git mount expansion
- Generate entrypoint permission fixer to chown home/mounts at startup
- Update base.dockerfile with sudo/passwordless sudo for permission fixer
- Add unit tests for manifest compiler, instance service, and migrations
- Add placeholder integration test for container lifecycle
- Update openspec/tasks/home-path-expansion.md task checkboxes
- Update project maps for modified files
Quality gates: py_compile, ruff, mypy, pytest tests/unit (205 passed),
pytest tests/integration (110 passed, 35 skipped). Alembic round-trip
and container lifecycle integration tests require Docker/PostgreSQL.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: apps/api/tests
|
||||
|
||||
## role
|
||||
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||
Provides shared test infrastructure and fixtures for the API application's test suite.
|
||||
## parent
|
||||
index: apps/api/.pi-map.index.md
|
||||
map: apps/api/.pi-map.md
|
||||
|
||||
@@ -4,11 +4,11 @@ dir: apps/api/tests
|
||||
index: apps/api/tests/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides shared test infrastructure and fixtures for the FastAPI API application.
|
||||
Provides shared test infrastructure and fixtures for the API application's test suite.
|
||||
## files
|
||||
- conftest.py | Provides shared pytest fixtures for testing a FastAPI application 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.user, src.models.project.project, src.models.project.git_repository, fastapi, sqlalchemy, aiosqlite, src.models, src.auth
|
||||
## arch
|
||||
Pytest fixture-based testing architecture using async SQLite in-memory database, dependency injection overrides, and async HTTP client setup for isolated integration tests.
|
||||
Pytest plugin architecture with dependency-injected async fixtures for database, HTTP client, and authentication state management.
|
||||
## tags
|
||||
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/api/tests/integration
|
||||
|
||||
## role
|
||||
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories.
|
||||
Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
|
||||
## parent
|
||||
index: apps/api/tests/.pi-map.index.md
|
||||
map: apps/api/tests/.pi-map.md
|
||||
@@ -21,6 +21,7 @@ map: apps/api/tests/.pi-map.md
|
||||
- test_projects_api.py
|
||||
- test_seed.py
|
||||
- test_ssh_keys_api.py
|
||||
- test_tool_instance_lifecycle.py
|
||||
- test_tool_types_api.py
|
||||
- test_tool_types_api_extended.py
|
||||
- test_users_api.py
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: apps/api/tests/integration
|
||||
index: apps/api/tests/integration/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Integration test suite for the API backend, covering authentication, workspaces, projects, notifications, configuration, and database models with real PostgreSQL and git repositories.
|
||||
Integration and unit test suite for the API backend, covering authentication, CRUD APIs, workspace operations, git integration, notifications, and database model validation against real PostgreSQL and mocked dependencies.
|
||||
## files
|
||||
- __init__.py | Empty file with no functionality
|
||||
- test_auth_api.py | Integration tests for authentication API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, 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.user, asyncpg, fastapi
|
||||
@@ -18,6 +18,7 @@ Integration test suite for the API backend, covering authentication, workspaces,
|
||||
- test_projects_api.py | Integration tests for a FastAPI projects API endpoint, verifying authentication, CRUD operations, and ownership-based authorization against a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, 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, 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_project(project_id: str, owner_id: str, name) → None, call:create_async_engine, call:build_database_url, call:async_sessionmaker, call:session_factory, call:Project, 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:Project, call:uuid.UUID, call:session.merge, call:session.commit, call:engine.dispose, func:test_create_project_requires_authentication() → None, call:_prepare_test_db, call:_load_app, call:TestClient, call:client.post, func:test_create_project_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_list_projects_returns_only_owned_projects() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.get, call:response.json, call:len, func:test_update_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, func:test_update_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch, call:response.json, func:test_delete_project_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_delete_project_successfully() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.delete, func:test_set_default_ssh_key_requires_ownership() → None, call:_prepare_test_db, call:_insert_user, call:_insert_project, call:_load_app, call:TestClient, call:client.cookies.set, call:_mint_token, call:client.patch | dep: uuid, datetime, asyncio, pytest, fastapi.testclient, sqlalchemy, sqlalchemy.ext.asyncio, src.auth.session, src.config, src.models, src.models.project.project, src.models.user.user, asyncpg, importlib, fastapi, src.database, src.api.user.auth, src.api.project.projects, src.main
|
||||
- test_seed.py | Tests deterministic seed user generation and database seeding functionality for development environments. | exp: func:test_build_seed_user_returns_deterministic_payload() → None, call:build_seed_user, func:test_seed_database_creates_development_user(db_session: AsyncSession) → None, call:seed_database, call:db_session.scalar, call:select(User).where | dep: pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.user, src.scripts.seed
|
||||
- test_ssh_keys_api.py | Integration tests verifying SSH key API endpoints require authentication | exp: func:test_create_ssh_key_requires_authentication(test_client: TestClient) → None, call:test_client.post, func:test_list_ssh_keys_requires_authentication(test_client: TestClient) → None, call:test_client.get | dep: pytest, fastapi.testclient
|
||||
- test_tool_instance_lifecycle.py | Integration test placeholder for verifying Docker availability and container home directory lifecycle behavior | exp: func:_docker_available() → bool, call:subprocess.run, func:test_docker_available_placeholder() → None, call:_docker_available | dep: subprocess, pytest
|
||||
- test_tool_types_api.py | Integration tests for a FastAPI tool types REST API endpoint using a real PostgreSQL database. | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, 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.tool_type, src.models.user.user, asyncpg, importlib, fastapi
|
||||
- test_tool_types_api_extended.py | Integration tests for a FastAPI tool types API endpoint covering CRUD operations with extended fields like dockerfile templates, readiness probes, startup commands, and validation. | 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 | dep: pytest, fastapi.testclient, fastapi.testclient.TestClient
|
||||
- test_users_api.py | Integration tests for user profile API endpoints using a real PostgreSQL database | exp: func:_postgres_available() → bool, call:asyncpg.connect, call:conn.close, call:asyncio.run, call:_check, func:_check() → bool, call:asyncpg.connect, call:conn.close, 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, 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.user, asyncpg, importlib, fastapi, src.database, src.api.user.users, src.main
|
||||
@@ -26,7 +27,7 @@ Integration test suite for the API backend, covering authentication, workspaces,
|
||||
- test_workspace_instances.py | Integration tests for FastAPI workspace instance endpoints (list and create) using an authenticated test client and mocked database fixtures. | 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
|
||||
Pytest-based integration testing using real external dependencies (PostgreSQL, git repos), FastAPI TestClient with authenticated fixtures, mocked database fixtures for isolation, and security validation patterns (cookie/ OIDC/ SSE auth).
|
||||
Pytest-based integration testing with FastAPI TestClient, SQLAlchemy async sessions, real PostgreSQL database fixtures, and selective mocking; follows pattern of endpoint-per-test-file with authenticated client fixtures and seeded database state.
|
||||
## tags
|
||||
test, call:, call:db, call:authenticated, user, create, call:response.json, call:create
|
||||
## symbols
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Integration tests for tool container home directory behavior.
|
||||
|
||||
These tests exercise container lifecycle behavior and require Docker and a
|
||||
running PostgreSQL database. They are skipped when Docker is unavailable.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _docker_available() -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "info"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.skipif(not _docker_available(), reason="Docker not available")
|
||||
def test_docker_available_placeholder() -> None:
|
||||
"""Placeholder to keep the test file valid when Docker is present.
|
||||
|
||||
Real lifecycle tests (container starts with HOME=/home/user, /workspace
|
||||
symlink works, git mounts are writable) should be added here once the
|
||||
test harness can start the API and Docker services.
|
||||
"""
|
||||
assert _docker_available()
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/api/tests/unit
|
||||
|
||||
## role
|
||||
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||
Unit test suite for the API application covering core services, utilities, and infrastructure components.
|
||||
## parent
|
||||
index: apps/api/tests/.pi-map.index.md
|
||||
map: apps/api/tests/.pi-map.md
|
||||
@@ -10,6 +10,7 @@ map: apps/api/tests/.pi-map.md
|
||||
-
|
||||
## files
|
||||
- __init__.py
|
||||
- test_alembic_migrations.py
|
||||
- test_config.py
|
||||
- test_config_profile_resolver.py
|
||||
- test_docker_build.py
|
||||
@@ -21,6 +22,7 @@ map: apps/api/tests/.pi-map.md
|
||||
- test_git_url_parser.py
|
||||
- test_health_monitor.py
|
||||
- test_home_path_expansion.py
|
||||
- test_instance_service.py
|
||||
- test_lifecycle_hooks.py
|
||||
- test_manifest_compiler.py
|
||||
- test_migration_metadata.py
|
||||
@@ -35,7 +37,7 @@ index: apps/api/tests/unit/.pi-map.index.md
|
||||
map: apps/api/tests/unit/.pi-map.md
|
||||
## workflows
|
||||
- change unit behavior
|
||||
read: __init__.py, test_config.py, test_config_profile_resolver.py
|
||||
read: __init__.py, test_alembic_migrations.py, test_config.py
|
||||
- change unit config
|
||||
read: test_config.py, test_config_profile_resolver.py
|
||||
## dirty
|
||||
|
||||
@@ -4,9 +4,10 @@ dir: apps/api/tests/unit
|
||||
index: apps/api/tests/unit/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Contains comprehensive unit tests for the API backend services covering configuration, Docker operations, Git integration, file management, health monitoring, notifications, and SSH key handling.
|
||||
Unit test suite for the API application covering core services, utilities, and infrastructure components.
|
||||
## files
|
||||
- __init__.py | Empty file with no functionality
|
||||
- test_alembic_migrations.py | Unit tests that verify Alembic database migrations are importable, have correct identifiers, and declare expected dependencies without requiring a live database. | exp: func:test_home_directory_migration_imports_and_rewrites() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable, func:test_merge_migration_resolves_heads() → None, call:Path, call:migration_path.exists, call:importlib.util.spec_from_file_location, call:importlib.util.module_from_spec, call:spec.loader.exec_module, call:callable | dep: importlib.util, pathlib, pytest, importlib
|
||||
- test_config.py | Tests configuration settings and database URL building for an application, verifying defaults, environment variable overrides, and environment-specific behavior. | exp: func:test_settings_default_database_url_uses_asyncpg(monkeypatch) → None, call:monkeypatch.delenv, 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
|
||||
- test_config_profile_resolver.py | Tests the config profile resolution system including merge helpers, profile inheritance with cycle detection, and git mount normalization | exp: class:TestMergeFunctions, method:test_merge_env_vars_basic(self) → None, call:_merge_env_vars, method:test_merge_env_vars_tracks_overrides(self) → None, call:_merge_env_vars, method:test_merge_runtime_hints_basic(self) → None, call:_merge_runtime_hints, method:test_merge_files_basic(self) → None, call:_merge_files, method:test_merge_mounts_basic(self) → None, call:_merge_mounts, method:test_merge_mounts_file_override(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_mounts_mode_conflict(self) → None, call:_merge_mounts, call:ResolvedMount, method:test_merge_git_mounts_basic(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_concatenate_same_repo_branch(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_dedup_same_mapping(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_repos(self) → None, call:_merge_git_mounts, call:len, method:test_merge_git_mounts_different_branches(self) → None, call:_merge_git_mounts, call:len, call:m.get, class:TestResolveProfile, class:TestApplyResolvedProfile, method:test_mounts_directory_not_individual_files(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path(volumes[0]["source"]).is_dir, call:(Path(volumes[0]["source"]) / "config.json").exists, call:(Path(volumes[0]["source"]) / "nested" / "file.txt").exists, method:test_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:Path, call:(Path(volumes[0]["source"]) / "z.json").exists, method:test_empty_mount_produces_no_volumes(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, method:test_home_expansion_in_directory_mount_target(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:(Path(volumes[0]["source"]) / "app.toml").exists, call:Path, method:test_readonly_mount_sets_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, method:test_writable_mount_does_not_set_readonly_flag(self, tmp_path) → None, call:ResolvedProfile, call:uuid.uuid4, call:ResolvedMount, call:apply_resolved_profile, call:str, call:len, call:volumes[0].get, class:TestCheckIncludeCycle | dep: uuid, pathlib, pytest, sqlalchemy.ext.asyncio, src.models.config.config_profile, src.services.config.config_profile_resolver
|
||||
- test_docker_build.py | Unit tests for a Docker image build service that verifies Dockerfile creation, command structure, context file handling, path traversal prevention, and error handling. | exp: class:TestBuildImage | dep: subprocess, tempfile, pathlib, unittest.mock, pytest, src.services.build.docker_build
|
||||
@@ -18,8 +19,9 @@ Contains comprehensive unit tests for the API backend services covering configur
|
||||
- test_git_url_parser.py | Tests for git URL parsing utilities that extract base repository URLs, validate clone URLs, and parse various git URL formats across GitHub, GitLab, and Bitbucket. | exp: class:TestExtractBaseRepoUrl, method:test_github_tree_url(self), call:extract_base_repo_url, method:test_github_blob_url(self), call:extract_base_repo_url, method:test_github_pull_url(self), call:extract_base_repo_url, method:test_github_issues_url(self), call:extract_base_repo_url, method:test_github_valid_url(self), call:extract_base_repo_url, method:test_github_url_with_query_params(self), call:extract_base_repo_url, method:test_gitlab_tree_url(self), call:extract_base_repo_url, method:test_gitlab_blob_url(self), call:extract_base_repo_url, method:test_gitlab_merge_request_url(self), call:extract_base_repo_url, method:test_gitlab_valid_url(self), call:extract_base_repo_url, method:test_bitbucket_src_url(self), call:extract_base_repo_url, method:test_bitbucket_valid_url(self), call:extract_base_repo_url, method:test_ssh_url(self), call:extract_base_repo_url, method:test_ssh_url_without_git_suffix(self), call:extract_base_repo_url, method:test_invalid_url(self), call:extract_base_repo_url, method:test_empty_url(self), call:extract_base_repo_url, class:TestIsValidCloneUrl, method:test_valid_ssh_url(self), call:is_valid_clone_url, method:test_valid_https_url(self), call:is_valid_clone_url, method:test_browser_url(self), call:is_valid_clone_url, method:test_url_without_git_suffix(self), call:is_valid_clone_url, method:test_invalid_url(self), call:is_valid_clone_url, class:TestParseGitUrl, method:test_valid_git_url(self), call:parse_git_url, method:test_browser_url(self), call:parse_git_url, method:test_invalid_url(self), call:parse_git_url, method:test_empty_url(self), call:parse_git_url, method:test_ssh_url(self), call:parse_git_url | dep: src.utils.git_url_parser, pytest
|
||||
- test_health_monitor.py | Unit tests for HealthMonitor state-transition logic covering container crash detection, tunnel failure detection, recovery detection, write deduplication, exception resilience, and start/stop lifecycle. | exp: func:event_bus() → InstanceEventBus, call:InstanceEventBus, call:bus._reset_for_testing, func:health_monitor(event_bus: InstanceEventBus) → HealthMonitor, call:HealthMonitor, func:_create_running_instance(db_session) → ToolInstance, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, func:test_detects_container_crash(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_tunnel_failure(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_detects_recovery(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:db_session.commit, call:HealthSnapshot, call:events_captured.append, call:event_bus.subscribe, call:patch, call:health_monitor._check_instance, call:db_session.refresh, call:len, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_skips_writes_when_no_state_change(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:HealthSnapshot, call:patch, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:len, call:result.scalars().all, func:test_docker_exception_resilience(db_session, event_bus: InstanceEventBus, health_monitor: HealthMonitor) → None, call:_create_running_instance, call:events_captured.append, call:event_bus.subscribe, call:patch, call:RuntimeError, call:health_monitor._check_instance, call:db_session.execute, call:select(HealthCheck).where, call:result.scalar_one_or_none, func:capture_event(payload: InstanceEventPayload) → None, call:events_captured.append, func:test_monitor_start_stop(health_monitor: HealthMonitor) → None, call:health_monitor.start, call:task.done, call:health_monitor.stop, call:suppress, call:task.cancelled | dep: asyncio, uuid, contextlib, unittest.mock, pytest, sqlalchemy, src.models.system.health_check, src.models.tool.tool_instance, src.models.user.user, src.services.instance.event_bus, src.services.instance.health_monitor
|
||||
- test_home_path_expansion.py | Unit tests for home directory path expansion (~ and $HOME) in container paths and manifest home directory resolution. | exp: class:TestExpandContainerPath, method:test_tilde_slash_expands(self) → None, call:expand_container_path, method:test_tilde_alone_expands(self) → None, call:expand_container_path, method:test_dollar_home_slash_expands(self) → None, call:expand_container_path, method:test_dollar_home_alone_expands(self) → None, call:expand_container_path, method:test_absolute_path_unchanged(self) → None, call:expand_container_path, method:test_relative_path_unchanged(self) → None, call:expand_container_path, method:test_tilde_in_middle_unchanged(self) → None, call:expand_container_path, method:test_dollar_home_in_middle_unchanged(self) → None, call:expand_container_path, method:test_root_home(self) → None, call:expand_container_path, class:TestGetManifestHomeDir, method:test_with_user_block(self) → None, call:get_manifest_home_dir, method:test_without_user_block(self) → None, call:get_manifest_home_dir, method:test_with_empty_user_name(self) → None, call:get_manifest_home_dir, method:test_with_none_user_name(self) → None, call:get_manifest_home_dir | dep: pytest, src.services.config.config_profile_resolver, src.services.build.manifest_compiler
|
||||
- test_instance_service.py | Unit tests for home directory expansion in docker-compose file modifications. | exp: class:TestModifyComposeFile, method:test_extra_volumes_expand_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text, method:test_working_directory_expands_home_dir(self, tmp_path), call:compose_path.write_text, call:modify_compose_file, call:str, call:compose_path.read_text | dep: pytest, src.services.tool.instance_service
|
||||
- test_lifecycle_hooks.py | Unit tests for lifecycle hook helper functions that derive notification titles and determine whether events should trigger notifications. | exp: class:TestDeriveTitle, method:test_known_event_types(self) → None, call:_derive_title, method:test_unknown_event_type(self) → None, call:_derive_title, class:TestShouldNotify, method:test_error_events_are_notified(self) → None, call:_should_notify, method:test_health_changed_running_is_notified(self) → None, call:_should_notify, method:test_created_started_stopped_restarted_deleted_filtered(self) → None, call:_should_notify, method:test_health_changed_non_running_filtered(self) → None, call:_should_notify | dep: pytest, src.services.instance.lifecycle_hooks
|
||||
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles with user configuration and home directory setup | exp: func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler
|
||||
- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Docker configurations (Dockerfile, compose, entrypoint) based on manifest specifications. | exp: class:TestGetManifestHomeDir, method:test_home_directory_in_manifest_wins(self) → None, call:get_manifest_home_dir, method:test_user_name_derives_home(self) → None, call:get_manifest_home_dir, method:test_root_fallback(self) → None, call:get_manifest_home_dir, method:test_empty_home_directory_falls_back(self) → None, call:get_manifest_home_dir, class:TestCompileDockerfileHomeDirectory, method:test_env_home_and_workdir_use_home_directory(self) → None, call:compile_dockerfile, method:test_workspace_symlink_created(self) → None, call:compile_dockerfile, method:test_runtime_working_dir_overrides_home_workdir(self) → None, call:compile_dockerfile, method:test_working_dir_expands_tilde(self) → None, call:compile_dockerfile, class:TestCompileComposeHomeDirectory, method:test_default_repo_mount_synthesized(self) → None, call:compile_compose, method:test_explicit_repo_mount_preserved(self) → None, call:compile_compose, method:test_working_dir_expands_home(self) → None, call:compile_compose, class:TestCompileEntrypoint, method:test_entrypoint_creates_home_and_workspace(self) → None, call:compile_entrypoint, method:test_entrypoint_fixes_mount_owners(self) → None, call:compile_entrypoint, func:test_compile_dockerfile_creates_config_dirs_for_user() → None, call:compile_dockerfile, func:test_compile_dockerfile_no_user_does_not_create_home() → None, call:compile_dockerfile | dep: pytest, src.services.build.manifest_compiler
|
||||
- test_migration_metadata.py | Tests Alembic database migration files for correct table definitions and revision chain metadata | exp: func:test_initial_migration_defines_all_core_tables() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module, func:test_refresh_tokens_migration_has_expected_revision_chain() → None, call:Path(__file__).resolve, call:spec_from_file_location, call:module_from_spec, call:spec.loader.exec_module | dep: pytest, importlib.util, pathlib, pathlib.Path
|
||||
- test_monitoring_models.py | Unit tests verifying creation, persistence, and querying of monitoring models (InstanceEvent and HealthCheck) with database migration compatibility. | exp: func:test_instance_event_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.refresh, call:isinstance, func:test_health_check_creation(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:HealthCheck, call:db_session.refresh, call:isinstance, func:test_instance_event_query_by_instance(db_session) → None, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, call:ToolInstance, call:InstanceEvent, call:db_session.execute, call:select(InstanceEvent).where, call:result.scalar_one | dep: uuid, datetime, pytest, sqlalchemy, src.models.system.health_check, src.models.system.instance_event, src.models.tool.tool_instance, src.models.user.user
|
||||
- test_notification_service.py | Unit tests for NotificationService covering CRUD operations, filtering, sorting, and ownership isolation. | exp: func:notification_service() → NotificationService, call:NotificationService, func:user_a(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:user_b(db_session: AsyncSession) → User, call:User, call:uuid.uuid4, call:db_session.add, call:db_session.commit, func:test_create_notification(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:uuid.uuid4, func:test_list_notifications_orders_by_created_at_desc(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:datetime.now, call:timedelta, call:db_session.commit, call:db_session.refresh, call:notification_service.list_notifications, func:test_list_notifications_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.list_notifications, func:test_list_notifications_unread_only(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.list_notifications, func:test_get_unread_count(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_read, call:notification_service.get_unread_count, func:test_mark_read_sets_read_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.mark_read, func:test_mark_all_read_affects_all_unread(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count, func:test_dismiss_sets_dismissed_at(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:db_session.execute, call:select(Notification).where, call:result.scalar_one, func:test_mark_read_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.mark_read, func:test_dismiss_wrong_owner_raises(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:notification_service.create_notification, call:pytest.raises, call:notification_service.dismiss, func:test_list_notifications_mute_categories(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.list_notifications, func:test_get_unread_count_excludes_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:notification_service.create_notification, call:notification_service.dismiss, call:notification_service.get_unread_count, func:test_dismiss_all_affects_all_non_dismissed(db_session: AsyncSession, notification_service: NotificationService, user_a: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_dismiss_all_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.dismiss_all, call:notification_service.list_notifications, func:test_mark_all_read_affects_only_caller(db_session: AsyncSession, notification_service: NotificationService, user_a: User, user_b: User) → None, call:range, call:notification_service.create_notification, call:notification_service.mark_all_read, call:notification_service.get_unread_count | dep: uuid, datetime, pytest, sqlalchemy, sqlalchemy.ext.asyncio, src.models.system.notification, src.models.user.user, src.services.shared.notification_service, NotificationService, Notification, User, AsyncSession
|
||||
@@ -28,9 +30,9 @@ Contains comprehensive unit tests for the API backend services covering configur
|
||||
- test_readiness_probe.py | Unit tests for a Docker container readiness probe service that executes commands via docker exec with retry logic. | exp: class:TestExecuteProbe, class:TestIntegrationScenarios | dep: unittest.mock, src.services.shared.readiness_probe, subprocess
|
||||
- test_ssh_keys.py | Unit tests for SSH key preparation functionality including file creation, permissions, ownership, and error handling | exp: class:TestPrepareSshKeyFiles | dep: os, pathlib, unittest.mock, pytest, src.services.shared.ssh_keys
|
||||
## arch
|
||||
Standard Python unittest/pytest pattern with heavy mocking of external dependencies (subprocess, docker, filesystem) to test service layer logic in isolation, organized by functional domain with one test module per service component.
|
||||
pytest-based unit testing with heavy mocking of external dependencies (Docker, Git, subprocess, database) to test components in isolation without live services.
|
||||
## tags
|
||||
test, url, call:notification, git, call:, call:db, merge, src
|
||||
test, url, call:notification, git, home, call:, merge, call:db
|
||||
## symbols
|
||||
- TestMergeFunctions
|
||||
- TestResolveProfile
|
||||
@@ -42,7 +44,7 @@ test, url, call:notification, git, call:, call:db, merge, src
|
||||
- TestSortVolumesBySpecificity
|
||||
## workflows
|
||||
- change unit behavior
|
||||
read: __init__.py, test_config.py, test_config_profile_resolver.py
|
||||
read: __init__.py, test_alembic_migrations.py, test_config.py
|
||||
- change unit config
|
||||
read: test_config.py, test_config_profile_resolver.py
|
||||
## dirty
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Unit tests for Alembic migration structure/import.
|
||||
|
||||
Actual upgrade/downgrade round-trips require a PostgreSQL database, so these
|
||||
tests verify that migrations are importable, have the expected identifiers,
|
||||
and declare the expected dependencies.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_home_directory_migration_imports_and_rewrites() -> None:
|
||||
migration_path = Path(__file__).parent.parent.parent / (
|
||||
"alembic/versions/2026_06_14_104415_add_tool_type_home_directory.py"
|
||||
)
|
||||
assert migration_path.exists()
|
||||
|
||||
spec = importlib.util.spec_from_file_location("home_dir_migration", migration_path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
assert module.revision == "2026_06_14_104415"
|
||||
assert module.down_revision == "f3d2dc90ba3a"
|
||||
assert callable(module.upgrade)
|
||||
assert callable(module.downgrade)
|
||||
assert module.OLD_WORKSPACE == "/workspace"
|
||||
assert module.NEW_WORKSPACE == "/home/user/{{WORKSPACE_NAME}}"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_merge_migration_resolves_heads() -> None:
|
||||
migration_path = Path(__file__).parent.parent.parent / (
|
||||
"alembic/versions/fc8f1a20cbf6_merge_home_directory_and_pi_agent_mount_.py"
|
||||
)
|
||||
assert migration_path.exists()
|
||||
|
||||
spec = importlib.util.spec_from_file_location("merge_migration", migration_path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
assert module.revision == "fc8f1a20cbf6"
|
||||
assert "2026_06_14_104415" in module.down_revision
|
||||
assert "8c6d1dbd4798" in module.down_revision
|
||||
assert callable(module.upgrade)
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Unit tests for the tool instance service."""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.tool.instance_service import modify_compose_file
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestModifyComposeFile:
|
||||
"""Tests for modify_compose_file home-directory expansion."""
|
||||
|
||||
def test_extra_volumes_expand_home_dir(self, tmp_path):
|
||||
compose_path = tmp_path / "docker-compose.yml"
|
||||
compose_path.write_text(
|
||||
'services:\n app:\n image: test:latest\n volumes: []\n'
|
||||
)
|
||||
|
||||
modify_compose_file(
|
||||
str(compose_path),
|
||||
extra_volumes=[
|
||||
{"source": "/host/config", "target": "~/.config", "type": "bind"},
|
||||
{"source": "/host/code", "target": "$HOME/code", "type": "bind"},
|
||||
],
|
||||
home_dir="/home/user",
|
||||
)
|
||||
|
||||
content = compose_path.read_text()
|
||||
assert "/host/config:/home/user/.config" in content
|
||||
assert "/host/code:/home/user/code" in content
|
||||
|
||||
def test_working_directory_expands_home_dir(self, tmp_path):
|
||||
compose_path = tmp_path / "docker-compose.yml"
|
||||
compose_path.write_text(
|
||||
'services:\n app:\n image: test:latest\n'
|
||||
)
|
||||
|
||||
modify_compose_file(
|
||||
str(compose_path),
|
||||
working_directory="~/workspace",
|
||||
home_dir="/home/user",
|
||||
)
|
||||
|
||||
content = compose_path.read_text()
|
||||
assert "working_dir: /home/user/workspace" in content
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.build.manifest_compiler import compile_dockerfile
|
||||
from src.services.build.manifest_compiler import (
|
||||
compile_compose,
|
||||
compile_dockerfile,
|
||||
compile_entrypoint,
|
||||
get_manifest_home_dir,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -36,3 +41,185 @@ def test_compile_dockerfile_no_user_does_not_create_home() -> None:
|
||||
|
||||
assert "useradd" not in dockerfile
|
||||
assert "/home/" not in dockerfile
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGetManifestHomeDir:
|
||||
"""Tests for get_manifest_home_dir precedence."""
|
||||
|
||||
def test_home_directory_in_manifest_wins(self) -> None:
|
||||
manifest = {
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev"},
|
||||
}
|
||||
assert get_manifest_home_dir(manifest) == "/home/custom"
|
||||
|
||||
def test_user_name_derives_home(self) -> None:
|
||||
manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}}
|
||||
assert get_manifest_home_dir(manifest) == "/home/dev"
|
||||
|
||||
def test_root_fallback(self) -> None:
|
||||
manifest = {"base_image": "ubuntu:24.04"}
|
||||
assert get_manifest_home_dir(manifest) == "/root"
|
||||
|
||||
def test_empty_home_directory_falls_back(self) -> None:
|
||||
manifest = {"home_directory": "", "user": {"name": "dev"}}
|
||||
assert get_manifest_home_dir(manifest) == "/home/dev"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompileDockerfileHomeDirectory:
|
||||
"""Tests that compile_dockerfile honors manifest.home_directory."""
|
||||
|
||||
def test_env_home_and_workdir_use_home_directory(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
}
|
||||
dockerfile = compile_dockerfile(manifest)
|
||||
|
||||
assert "ENV HOME=/home/custom" in dockerfile
|
||||
assert "WORKDIR /home/custom" in dockerfile
|
||||
|
||||
def test_workspace_symlink_created(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
}
|
||||
dockerfile = compile_dockerfile(manifest)
|
||||
|
||||
assert "ln -sfn /home/custom/{{WORKSPACE_NAME}} /workspace" in dockerfile
|
||||
|
||||
def test_runtime_working_dir_overrides_home_workdir(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
"runtime": {"working_dir": "/app/code"},
|
||||
}
|
||||
dockerfile = compile_dockerfile(manifest)
|
||||
|
||||
assert "WORKDIR /app/code" in dockerfile
|
||||
assert "WORKDIR /home/custom" not in dockerfile
|
||||
|
||||
def test_working_dir_expands_tilde(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
"runtime": {"working_dir": "~/code"},
|
||||
}
|
||||
dockerfile = compile_dockerfile(manifest)
|
||||
|
||||
assert "WORKDIR /home/custom/code" in dockerfile
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompileComposeHomeDirectory:
|
||||
"""Tests that compile_compose uses home_directory for volumes and working_dir."""
|
||||
|
||||
def test_default_repo_mount_synthesized(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
}
|
||||
variables = {
|
||||
"IMAGE_TAG": "test:latest",
|
||||
"INSTANCE_NAME": "test-instance",
|
||||
"REPO_PATH": "/host/repos/my-app",
|
||||
"WORKSPACE_NAME": "my-app",
|
||||
"TOOL_PORT": 0,
|
||||
"EXTRA_ENV": {},
|
||||
"EXTRA_VOLUMES": [],
|
||||
}
|
||||
compose = compile_compose(manifest, variables)
|
||||
|
||||
assert "/host/repos/my-app:/home/custom/my-app" in compose
|
||||
|
||||
def test_explicit_repo_mount_preserved(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
"mounts": [
|
||||
{"source_type": "repo", "target": "/opt/code", "readonly": True}
|
||||
],
|
||||
}
|
||||
variables = {
|
||||
"IMAGE_TAG": "test:latest",
|
||||
"INSTANCE_NAME": "test-instance",
|
||||
"REPO_PATH": "/host/repos/my-app",
|
||||
"WORKSPACE_NAME": "my-app",
|
||||
"TOOL_PORT": 0,
|
||||
"EXTRA_ENV": {},
|
||||
"EXTRA_VOLUMES": [],
|
||||
}
|
||||
compose = compile_compose(manifest, variables)
|
||||
|
||||
assert "/host/repos/my-app:/opt/code:ro" in compose
|
||||
assert "/home/custom/my-app" not in compose
|
||||
|
||||
def test_working_dir_expands_home(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
"runtime": {"working_dir": "$HOME/code"},
|
||||
}
|
||||
variables = {
|
||||
"IMAGE_TAG": "test:latest",
|
||||
"INSTANCE_NAME": "test-instance",
|
||||
"REPO_PATH": "/host/repos/my-app",
|
||||
"WORKSPACE_NAME": "my-app",
|
||||
"TOOL_PORT": 0,
|
||||
"EXTRA_ENV": {},
|
||||
"EXTRA_VOLUMES": [],
|
||||
}
|
||||
compose = compile_compose(manifest, variables)
|
||||
|
||||
assert "working_dir: /home/custom/code" in compose
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCompileEntrypoint:
|
||||
"""Tests for the generated permission-fixing entrypoint."""
|
||||
|
||||
def test_entrypoint_creates_home_and_workspace(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
}
|
||||
entrypoint = compile_entrypoint(manifest)
|
||||
|
||||
assert 'mkdir -p "$HOME_DIR"' in entrypoint
|
||||
assert 'mkdir -p "$WORKSPACE_TARGET"' in entrypoint
|
||||
assert 'ln -sfn "$WORKSPACE_TARGET" /workspace' in entrypoint
|
||||
|
||||
def test_entrypoint_fixes_mount_owners(self) -> None:
|
||||
manifest = {
|
||||
"base_image": "ubuntu:24.04",
|
||||
"interface_type": "terminal",
|
||||
"home_directory": "/home/custom",
|
||||
"user": {"name": "dev", "uid": 1000, "gid": 1000},
|
||||
"mounts": [
|
||||
{"target": "~/.config", "readonly": False},
|
||||
{"target": "/opt/readonly", "readonly": True},
|
||||
],
|
||||
}
|
||||
entrypoint = compile_entrypoint(manifest)
|
||||
|
||||
assert 'fix_owner "/home/custom/.config"' in entrypoint
|
||||
assert 'fix_owner "/opt/readonly"' not in entrypoint
|
||||
|
||||
|
||||
Reference in New Issue
Block a user