fix: force interactive login shell for bash containers

Detached containers with tty: true still exited immediately because a
plain /bin/bash invocation exits with code 0 when stdin is not connected.

- Detect when the container CMD is /bin/bash or bash and exec an
  interactive login shell () after dropping privileges
- Keep the generic  path for non-shell commands
- Bump compiler_version to v4 to force a fresh image build

Quality gates:
- pytest tests/unit: 219 passed
- ruff: clean on changed files
- mypy: clean on changed files
This commit is contained in:
Developer
2026-06-15 11:18:28 +00:00
parent ed1f7805f6
commit 29e48cdb65
12 changed files with 24 additions and 16 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests
## role
Provides shared test infrastructure and fixtures for API integration testing
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
+2 -2
View File
@@ -4,11 +4,11 @@ dir: apps/api/tests
index: apps/api/tests/.pi-map.index.md
## role
Provides shared test infrastructure and fixtures for API integration testing
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 architecture with async SQLite test database, dependency injection overrides, and authenticated client factories for FastAPI endpoint testing
Pytest fixture-based testing architecture with async SQLite test database, dependency injection overrides, and authenticated client factories using FastAPI's TestClient.
## tags
call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user
## symbols
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/api/tests/unit
## role
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
Contains unit tests for the API application's core services, utilities, and infrastructure components.
## parent
index: apps/api/tests/.pi-map.index.md
map: apps/api/tests/.pi-map.md
+2 -2
View File
@@ -4,7 +4,7 @@ dir: apps/api/tests/unit
index: apps/api/tests/unit/.pi-map.index.md
## role
Contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components.
Contains unit tests for the API application's 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 revision 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, func:test_remove_pi_agent_repo_mount_migration_imports() → 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
@@ -30,7 +30,7 @@ Contains comprehensive unit tests for the API application's core services, utili
- 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 use of mocking (subprocess, filesystem, database) to isolate components; tests are organized by service/domain with each file targeting a single module/class; no internal dependencies between test files.
Comprehensive unit test suite using mocked dependencies to test business logic in isolation, covering database migrations, configuration, Docker operations, Git workflows, file services, event handling, health monitoring, and SSH/security utilities.
## tags
test, url, call:notification, git, call:, home, merge, call:db
## symbols
@@ -270,6 +270,7 @@ def test_compile_dockerfile_starts_as_root_and_drops_privileges() -> None:
entrypoint = compile_entrypoint(manifest)
assert "USER dev" not in dockerfile
assert 'exec runuser -u dev -- /bin/bash -il' in entrypoint
assert 'exec runuser -u dev -- "$@"' in entrypoint