feat: complete workspace-first-ui cleanup and tests

- Delete dead repo-workspace code: RepoWorkspacePage, useRepoWorkspace,
  WorkspaceLayout, FileBrowser, old git components (git-toolbar, file-editor,
  commit-panel), and repo-workspace.css.
- Fix stale backend test imports for moved models/services.
- Add GitOperations unit tests.
- Add integration tests for workspace files, git, and instances endpoints.
- Add frontend tests for WorkspaceDetailPage and ProjectCard.
- Update OpenSpec workspace-first-ui tasks and mark change completed.
- Regenerate project maps.

Quality gates: npm run typecheck, npm run lint, npm test -- --run (87 passed),
python3 -m py_compile on changed backend files, pytest backend workspace tests.
This commit is contained in:
Developer
2026-06-12 17:25:09 +00:00
parent aa49efb236
commit c26e9eacfa
49 changed files with 993 additions and 2057 deletions
+4 -1
View File
@@ -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
+10 -7
View File
@@ -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
@@ -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
@@ -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"
@@ -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"
@@ -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: