diff --git a/.pi-map.index.md b/.pi-map.index.md index 38b198c..6325906 100644 --- a/.pi-map.index.md +++ b/.pi-map.index.md @@ -16,7 +16,7 @@ dir: . Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration for a self-hosted project management platform with AI-assisted development workflows. +A self-hosted web-based project management and git repository platform with OAuth2 authentication, containerized via Docker Compose. ## parent - ## children diff --git a/.pi-map.md b/.pi-map.md index 6e626c7..03088ba 100644 --- a/.pi-map.md +++ b/.pi-map.md @@ -18,7 +18,7 @@ index: ./.pi-map.index.md Trust boundary: index routes, map orients, source decides. ## role -Infrastructure and deployment configuration for a self-hosted project management platform with AI-assisted development workflows. +A self-hosted web-based project management and git repository platform with OAuth2 authentication, containerized via Docker Compose. ## files - .env.example | Provides a template of environment variables for configuring a Headquarter application with PostgreSQL, Redis, Authentik SSO, and Docker/Traefik deployment - .gitignore | Specifies files and directories for Git to ignore across a multi-language project with Python, Node, and custom tooling | dep: Git @@ -31,7 +31,7 @@ Infrastructure and deployment configuration for a self-hosted project management - progress.md | Tracks completed and remaining tasks for a backend-frontend code refactoring project organized in 7 phases - swap-pane | Empty file with no functionality ## arch -Containerized microservices architecture using Docker Compose with PostgreSQL/Redis data layer, Traefik reverse proxy, and environment-driven configuration supporting OAuth2 SSO integration. +Multi-service containerized architecture with separate frontend (likely Node.js), API backend (likely Python), PostgreSQL database, Redis cache, and Traefik reverse proxy for TLS termination, configured through environment variables and supporting Authentik SSO integration. ## tags docker, redis, git, application, postgresql, compose, traefik, project ## symbols diff --git a/apps/.pi-map.index.md b/apps/.pi-map.index.md index cde8081..9e45461 100644 --- a/apps/.pi-map.index.md +++ b/apps/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps ## role -Contains the main deployable application modules that compose the project's executable entry points. +Contains the main deployable application modules and entry points for the project. ## parent index: ./.pi-map.index.md map: ./.pi-map.md diff --git a/apps/.pi-map.md b/apps/.pi-map.md index 4f8d214..5425fa8 100644 --- a/apps/.pi-map.md +++ b/apps/.pi-map.md @@ -4,10 +4,10 @@ dir: apps index: apps/.pi-map.index.md ## role -Contains the main deployable application modules that compose the project's executable entry points. +Contains the main deployable application modules and entry points for the project. ## files ## arch -Modular monolith or microservices architecture where each subdirectory represents an independent application with its own configuration, dependencies, and lifecycle. +Follows a modular app structure where each subdirectory is an independent application with its own configuration, dependencies, and build setup, typically using a monorepo pattern with shared libraries. ## tags - ## symbols diff --git a/apps/api/.pi-map.index.md b/apps/api/.pi-map.index.md index 9d442f1..0640d99 100644 --- a/apps/api/.pi-map.index.md +++ b/apps/api/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api ## role -Backend API service that provides a FastAPI-based management layer for projects, git repositories, and containerized development tools with database-backed state. +FastAPI backend API that manages projects, git repositories, and development tools through Docker-based instances with PostgreSQL persistence. ## parent index: apps/.pi-map.index.md map: apps/.pi-map.md diff --git a/apps/api/.pi-map.md b/apps/api/.pi-map.md index ed25a64..154883f 100644 --- a/apps/api/.pi-map.md +++ b/apps/api/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api index: apps/api/.pi-map.index.md ## role -Backend API service that provides a FastAPI-based management layer for projects, git repositories, and containerized development tools with database-backed state. +FastAPI backend API that manages projects, git repositories, and development tools through Docker-based instances with PostgreSQL persistence. ## files - .dockerignore | Specifies files and directories to exclude from Docker build context to reduce image size and avoid copying unnecessary files into containers. | dep: Docker - Dockerfile | Multi-stage Docker build for a Python application with Docker socket access, Cloudflare tunneling, and database dependency waiting | dep: python:3.11-slim, gcc, libpq-dev, docker-ce-cli, docker-compose-plugin, cloudflared, uvicorn, pyproject.toml dependencies @@ -14,7 +14,7 @@ Backend API service that provides a FastAPI-based management layer for projects, - uv.lock | Lock file for the uv Python package manager that pins exact dependency versions and their artifact hashes for reproducible installations | dep: uv, Python 3.11+, aiosqlite, alembic, annotated-doc, annotated-types, anyio, ast-serialize, asyncpg, and many other PyPI packages - wait-for-db.sh | Wait for a PostgreSQL database to become available before executing a command, with configurable retry logic. | dep: nc (netcat), sh (POSIX shell), sleep ## arch -Async Python/FastAPI with SQLAlchemy/Alembic PostgreSQL migrations, uv package management, multi-stage Docker containerization with Docker socket access for container orchestration, and Cloudflare tunneling for external connectivity. +Async Python/FastAPI service using multi-stage Docker containers, Alembic migrations, uv package management, and external Docker socket access for container orchestration. ## tags docker, alembic, python, database, fastapi, postgresql, asyncpg, uvicorn ## symbols diff --git a/apps/api/src/.pi-map.index.md b/apps/api/src/.pi-map.index.md index 95d7888..9112d64 100644 --- a/apps/api/src/.pi-map.index.md +++ b/apps/api/src/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src ## role -Core backend API package for the "Headquarter" service, handling configuration, database connectivity, structured logging, and FastAPI application initialization. +Core application package for the Headquarter API, handling configuration, database connectivity, logging infrastructure, and FastAPI application lifecycle. ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md diff --git a/apps/api/src/.pi-map.md b/apps/api/src/.pi-map.md index 56edc4d..6300dca 100644 --- a/apps/api/src/.pi-map.md +++ b/apps/api/src/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api/src index: apps/api/src/.pi-map.index.md ## role -Core backend API package for the "Headquarter" service, handling configuration, database connectivity, structured logging, and FastAPI application initialization. +Core application package for the Headquarter API, handling configuration, database connectivity, logging infrastructure, and FastAPI application lifecycle. ## files - __init__.py | Marks the directory as a Python package for the Headquarter API. - config.py | Defines application configuration settings with environment-based overrides using Pydantic, including database URLs, service domains, OAuth/Authentik integration, JWT/session settings, and computed properties for environment-specific behavior. | exp: class:Settings, func:build_database_url(user: str, password: str, host: str, port: int, database: str) → str | dep: pydantic, pydantic_settings @@ -12,7 +12,7 @@ Core backend API package for the "Headquarter" service, handling configuration, - logging_config.py | Configures structured JSON logging with correlation ID injection, custom formatters, and HTTP request/exception middleware for a FastAPI application. | exp: class:CorrelationIdFilter, method:filter(self, record: logging.LogRecord) → bool, call:get_correlation_id, class:JSONFormatter, method:format(self, record: logging.LogRecord) → str, call:self.formatTime, call:record.getMessage, call:getattr, call:self.formatException, call:json.dumps, method:formatTime(self, record: logging.LogRecord, datefmt) → str, call:time.strftime, call:time.gmtime, class:RequestLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:time.time, call:logger.info, call:call_next, call:int, call:logger.error, call:type, call:traceback.format_exc, class:ExceptionLoggingMiddleware, method:dispatch(self, request: Request, call_next: Callable) → Response, call:call_next, call:logger.critical, call:traceback.format_exc, func:configure_logging(level) → None, call:JSONFormatter, call:logging.StreamHandler, call:console_handler.setFormatter, call:console_handler.addFilter, call:CorrelationIdFilter, call:root_logger.setLevel, call:logging.getLogger("uvicorn").setLevel, call:logging.getLogger("uvicorn.access").setLevel, call:logging.getLogger("sqlalchemy.engine").setLevel, call:logger.info, call:logging.getLevelName | dep: json, logging, sys, time, traceback, collections.abc, fastapi, starlette.middleware.base, src.services.shared.correlation - main.py | Initializes and configures a FastAPI application for the "Headquarter API" with database setup, middleware, routing, and background services. | exp: func:_sanitize_validation_errors(errors), call:error.get, call:str, call:ctx.items, call:isinstance, call:type, call:sanitized.append, func:validation_exception_handler(request: Request, exc: RequestValidationError), call:exc.errors, call:logger.warning, call:_sanitize_validation_errors, call:JSONResponse, func:on_startup(), call:logger.info, call:init_database, call:logger.error, call:sys.exit, call:_health_monitor.start, call:seed_builtin_tool_types, func:on_shutdown(), call:logger.info, call:_health_monitor.stop | dep: logging, os, fastapi, fastapi.exceptions, fastapi.middleware.cors, fastapi.responses, fastapi.staticfiles, src.api.config, src.api.project, src.api.system, src.api.tool, src.api.user, src.api.workspace, src.config, src.models, src.database, src.logging_config, src.seeds.builtin_tool_types, src.services.instance, src.services.shared, sys, src.api.* ## arch -Layered architecture with environment-driven Pydantic config, async SQLAlchemy with retry resilience, structured JSON logging with correlation ID tracking, and modular FastAPI bootstrap with middleware/routing composition. +Layered architecture with Pydantic-based settings management, async SQLAlchemy with Alembic migrations, structured JSON logging with correlation ID tracking, and modular FastAPI initialization with middleware pipeline. ## tags src, database, logging, call:logger.info, api, middleware, fastapi, filter ## symbols diff --git a/apps/api/src/services/.pi-map.index.md b/apps/api/src/services/.pi-map.index.md index e6e5bdc..13b8c13 100644 --- a/apps/api/src/services/.pi-map.index.md +++ b/apps/api/src/services/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src/services ## role -Marks the `services` directory as a Python package for organizing business logic and service-layer modules. +Service layer package for the API application, intended to contain business logic implementations. ## parent index: apps/api/src/.pi-map.index.md map: apps/api/src/.pi-map.md diff --git a/apps/api/src/services/.pi-map.md b/apps/api/src/services/.pi-map.md index f8fcd79..734d647 100644 --- a/apps/api/src/services/.pi-map.md +++ b/apps/api/src/services/.pi-map.md @@ -4,11 +4,11 @@ dir: apps/api/src/services index: apps/api/src/services/.pi-map.index.md ## role -Marks the `services` directory as a Python package for organizing business logic and service-layer modules. +Service layer package for the API application, intended to contain business logic implementations. ## files - __init__.py | Empty file with no functionality ## arch -Standard Python package structure using `__init__.py` to define an importable namespace, following conventional layered architecture with an empty initializer awaiting future service modules. +Standard Python package structure with an empty initializer, awaiting service module implementations following a layered architecture pattern. ## tags init, empty, functionality ## symbols diff --git a/apps/api/src/services/build/manifest_compiler.py b/apps/api/src/services/build/manifest_compiler.py index a82e4b7..62d834f 100644 --- a/apps/api/src/services/build/manifest_compiler.py +++ b/apps/api/src/services/build/manifest_compiler.py @@ -388,12 +388,12 @@ def compile_entrypoint(manifest: dict) -> str: # Drop from root to the container user before running the real command. # The Dockerfile no longer sets USER, so the entrypoint has root for the - # setup above. Use `su -l` to start a login shell as the container user - # and exec the original CMD, preserving TTY/stdin for interactive tools. + # setup above. `runuser` (root-only, no PAM) preserves the environment, + # stdin, and TTY so interactive tools like bash keep running. if user: name = user["name"] lines.append("# Drop privileges to the container user") - lines.append(f'exec su -l {name} -s /bin/bash -c \'exec "$@"\' -- "$@"') + lines.append(f'exec runuser -u {name} -- "$@"') else: lines.append('exec "$@"') return "\n".join(lines) diff --git a/apps/api/src/services/config/config_profile_resolver.py b/apps/api/src/services/config/config_profile_resolver.py index 83a5ac0..2cfa1f5 100644 --- a/apps/api/src/services/config/config_profile_resolver.py +++ b/apps/api/src/services/config/config_profile_resolver.py @@ -172,6 +172,47 @@ def _merge_mounts( return result +def _find_mount_conflicts( + profile: ConfigProfile, + resolved: ResolvedProfile, +) -> list[dict[str, Any]]: + """Find mounts on the profile that override mounts from included profiles. + + Returns a list of conflict descriptors with the target path, the included + profile that originally provided the mount, and the current profile name. + """ + conflicts = [] + own_targets = {m["target"] for m in (profile.mounts or [])} + own_files = {f for m in (profile.mounts or []) for f in m.get("files", {}).keys()} + + for mount in resolved.mounts.values(): + if mount.target in own_targets: + # The profile itself has a mount at the same target as an included one. + conflicts.append( + { + "type": "mount_target", + "target": mount.target, + "overridden_by": profile.name, + "source": resolved.profile_name, + } + ) + continue + + for rel_path in mount.files: + if rel_path in own_files: + conflicts.append( + { + "type": "mount_file", + "target": mount.target, + "file": rel_path, + "overridden_by": profile.name, + "source": resolved.profile_name, + } + ) + + return conflicts + + def _merge_git_mounts( base: list[dict[str, Any]], overlay: list[dict[str, Any]], diff --git a/apps/api/src/services/docker/.pi-map.index.md b/apps/api/src/services/docker/.pi-map.index.md index 16f23a5..fe744f5 100644 --- a/apps/api/src/services/docker/.pi-map.index.md +++ b/apps/api/src/services/docker/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/src/services/docker ## role -Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure. +Provides Docker infrastructure automation for container lifecycle management, service composition, secure configuration deployment, and external network tunneling. ## parent index: apps/api/src/services/.pi-map.index.md map: apps/api/src/services/.pi-map.md diff --git a/apps/api/src/services/docker/.pi-map.md b/apps/api/src/services/docker/.pi-map.md index 51c80f6..44f617b 100644 --- a/apps/api/src/services/docker/.pi-map.md +++ b/apps/api/src/services/docker/.pi-map.md @@ -4,15 +4,15 @@ dir: apps/api/src/services/docker index: apps/api/src/services/docker/.pi-map.index.md ## role -Provides Docker infrastructure services for container lifecycle management, compose orchestration, secure configuration deployment, and external tunnel exposure. +Provides Docker infrastructure automation for container lifecycle management, service composition, secure configuration deployment, and external network tunneling. ## files - __init__.py | Package initialization file that exposes Docker-related service functions for container operations, compose management, configuration staging, and tunnel management. | dep: src.services.docker.compose, src.services.docker.config_staging, src.services.docker.container, src.services.docker.tunnel - compose.py | Generates, renders, and executes Docker Compose files with volume sorting and template variable substitution. | exp: func:sort_volumes_by_specificity(volumes: list[str]) → list[str], call:vol.split, call:len, call:parts[1].rstrip, call:target.count, call:targets.append, call:Counter(targets).items, call:logger.warning, call:sorted, func:_target_depth(vol: str) → int, call:vol.split, call:len, call:parts[1].rstrip, call:target.count, func:render_compose_template(template: str, variables: dict[str, Any]) → str, call:variables.items, call:result.replace, call:str, call:aliases.items, call:variables.get, func:write_compose_file(instance_dir: str, content: str) → str, call:Path, call:compose_path.write_text, call:str, func:execute_compose_command(compose_path: str, action: str, timeout, env_file) → tuple[int, str, str], call:Path, call:cmd.extend, call:cmd.append, call:subprocess.run, call:str, raise:ValueError | dep: logging, subprocess, collections, pathlib, typing, collections.Counter, pathlib.Path, typing.Any - config_staging.py | Stages configuration files into instance directories with security checks for path traversal. | exp: func:ensure_instance_directory(instance_id: str, base_path) → str, call:Settings, call:Path, call:instance_dir.mkdir, call:str, call:instance_dir.absolute, func:write_env_file(instance_dir: str, env_vars: dict[str, str]) → str, call:Path, call:env_vars.items, call:env_path.write_text, call:"\n".join, call:str, func:write_config_files(instance_dir: str, files: dict[str, str]) → None, call:Path, call:files.items, call:full_path.resolve().relative_to, call:instance_path.resolve, call:full_path.parent.mkdir, call:full_path.write_text, raise:ValueError | dep: logging, pathlib, src.config, src.config.Settings -- container.py | Provides Docker container runtime queries and network management utilities via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket +- container.py | Provides utility functions for querying Docker container runtime state, managing container network connections, and finding free TCP ports via subprocess calls to the Docker CLI. | exp: func:get_container_id(instance_name: str) → str | None, call:instance_name.lower, call:subprocess.run, call:result.stdout.strip, call:ps_result.stdout.strip().splitlines, call:line.split, call:len, call:name.lower, func:get_container_name(instance_name: str) → str | None, call:subprocess.run, call:instance_name.lower, call:result.stdout.strip().lstrip, func:get_backend_network_name() → str, call:subprocess.run, call:result.stdout.strip().split, call:net.lower, func:connect_container_to_network(container_name: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_ip_on_network(container_id: str, network_name) → str | None, call:get_backend_network_name, call:subprocess.run, call:result.stdout.strip, func:is_container_on_network(container_id: str, network_name) → bool, call:get_backend_network_name, call:subprocess.run, func:get_container_status(container_id: str) → dict[str, Any], call:subprocess.run, call:result.stdout.strip().split, call:int, call:len, call:parts[1].isdigit, func:wait_for_container_running(container_id: str, timeout, interval) → dict[str, Any], call:time.time, call:get_container_status, call:time.sleep, func:get_container_logs(container_id: str, tail) → str, call:subprocess.run, call:str, func:find_free_port(start, end) → int, call:range, call:socket.socket, call:s.connect_ex, raise:RuntimeError | dep: logging, subprocess, time, typing, socket - tunnel.py | Manages Cloudflare tunnels by orchestrating cloudflared Docker containers to expose internal services via temporary public URLs. | exp: func:_tunnel_container_name(instance_name: str) → str, call:instance_name.lower, func:_ensure_image() → None, call:subprocess.run, call:result.stdout.strip, call:logger.info, call:logger.warning, func:_cleanup_stale_tunnel(tunnel_name: str) → None, call:subprocess.run, func:_get_tunnel_logs(tunnel_name: str) → tuple[str, str], call:subprocess.run, func:_get_tunnel_exit_code(tunnel_name: str) → int | None, call:subprocess.run, call:int, call:result.stdout.strip, func:start_tunnel(instance_name: str, container_port: int, timeout, target_url) → dict[str, str], call:_ensure_image, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:instance_name.lower, call:get_backend_network_name, call:logger.debug, call:" ".join, call:subprocess.run, call:proc.stdout.strip, call:re.compile, call:__import__("time").time, call:_get_tunnel_logs, call:url_pattern.search, call:match.group, call:_get_tunnel_exit_code, call:__import__("time").sleep, call:logger.info, raise:RuntimeError, func:stop_tunnel(instance_name: str) → None, call:_tunnel_container_name, call:_cleanup_stale_tunnel, call:logger.debug, func:recreate_tunnel(instance_name: str, container_port: int, target_url) → dict[str, str], call:stop_tunnel, call:start_tunnel, func:check_tunnel_health(url: str, timeout) → dict[str, Any], call:subprocess.run, call:int, call:result.stdout.strip, call:str(exc).lower, call:any | dep: logging, re, subprocess, typing, src.services.docker.container ## arch -Service-oriented utility modules with subprocess-based Docker CLI integration, Jinja2 templating for compose generation, and security-hardened file operations with path traversal validation. +Subprocess-based CLI wrapper architecture with template-driven file generation, security-validated file staging, and container-orchestrated tunnel proxying. ## tags tunnel, container, call:subprocess.run, get, name, call:, network, call:result.stdout.strip ## symbols diff --git a/apps/api/src/services/docker/container.py b/apps/api/src/services/docker/container.py index ea1db4f..da7f6b0 100644 --- a/apps/api/src/services/docker/container.py +++ b/apps/api/src/services/docker/container.py @@ -283,7 +283,7 @@ def get_container_logs(container_id: str, tail: int = 100) -> str: tail: Number of lines to return Returns: - Container logs + Container logs (stdout + stderr) """ result = subprocess.run( ["docker", "logs", "--tail", str(tail), container_id], @@ -291,9 +291,15 @@ def get_container_logs(container_id: str, tail: int = 100) -> str: text=True, ) - if result.returncode == 0: - return result.stdout - return f"Failed to get logs: {result.stderr}" + if result.returncode != 0: + return f"Failed to get logs: {result.stderr}" + + logs = result.stdout + if result.stderr: + if logs: + logs += "\n" + logs += f"STDERR:\n{result.stderr}" + return logs def find_free_port(start: int = 10000, end: int = 20000) -> int: diff --git a/apps/api/tests/.pi-map.index.md b/apps/api/tests/.pi-map.index.md index f039017..a1951b9 100644 --- a/apps/api/tests/.pi-map.index.md +++ b/apps/api/tests/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests ## role -Provides shared testing infrastructure and fixtures for the API application's test suite. +Provides shared test infrastructure and fixtures for API integration testing ## parent index: apps/api/.pi-map.index.md map: apps/api/.pi-map.md diff --git a/apps/api/tests/.pi-map.md b/apps/api/tests/.pi-map.md index 9094464..68138d6 100644 --- a/apps/api/tests/.pi-map.md +++ b/apps/api/tests/.pi-map.md @@ -4,11 +4,11 @@ dir: apps/api/tests index: apps/api/tests/.pi-map.index.md ## role -Provides shared testing infrastructure and fixtures for the API application's test suite. +Provides shared test infrastructure and fixtures for API integration testing ## 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 using dependency injection for async database sessions, authenticated HTTP clients, and reusable test data factories. +Pytest fixture-based architecture with async SQLite test database, dependency injection overrides, and authenticated client factories for FastAPI endpoint testing ## tags call:app.dependency, call:create, overrides.get, call:override, fn, call:gen.asend, call:gen.aclose, user ## symbols diff --git a/apps/api/tests/unit/.pi-map.index.md b/apps/api/tests/unit/.pi-map.index.md index 6f736a7..a7e7245 100644 --- a/apps/api/tests/unit/.pi-map.index.md +++ b/apps/api/tests/unit/.pi-map.index.md @@ -2,7 +2,7 @@ dir: apps/api/tests/unit ## role -This package contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components. +Contains comprehensive 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 diff --git a/apps/api/tests/unit/.pi-map.md b/apps/api/tests/unit/.pi-map.md index 0cd7e71..cb1592f 100644 --- a/apps/api/tests/unit/.pi-map.md +++ b/apps/api/tests/unit/.pi-map.md @@ -4,7 +4,7 @@ dir: apps/api/tests/unit index: apps/api/tests/unit/.pi-map.index.md ## role -This package contains comprehensive unit tests for the API application's core services, utilities, and infrastructure components. +Contains comprehensive 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 @@ -21,7 +21,7 @@ This package contains comprehensive unit tests for the API application's core se - 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 tool instance service functions including compose file modification, repository mount name resolution, and manifest instance preparation | 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, class:TestGetRepositoryMountName, method:test_prefers_remote_url_name_over_user_provided_name(self), call:MagicMock, call:_get_repository_mount_name, method:test_parses_browser_url_to_repo_name(self), call:MagicMock, call:_get_repository_mount_name, method:test_uses_workspace_path_basename_when_workspace_provided(self), call:MagicMock, call:_get_repository_mount_name, method:test_falls_back_to_repo_name_when_remote_url_missing(self), call:MagicMock, call:_get_repository_mount_name, method:test_falls_back_to_repo_name_for_unparseable_url(self), call:MagicMock, call:_get_repository_mount_name, class:Result, func:test_prepare_manifest_instance_uses_workspace_path_basename(), call:MagicMock, call:AsyncMock, call:Result, call:prepare_manifest_instance, func:session_get(model, obj_id), func:fake_run(cmd), call:Result | dep: unittest.mock, pytest, src.services.tool.instance_service, subprocess, src.services.tool - 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, docker-compose files, and entrypoint scripts based on manifest configurations. | 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_workspace_not_baked_into_image(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_workspace_name_substituted_in_mount_target(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_removes_stale_placeholder_directory(self) → None, call:compile_entrypoint, method:test_entrypoint_uses_root_then_sudo_for_workspace_symlink(self) → None, call:compile_entrypoint, call:entrypoint.find, 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, func:test_compile_dockerfile_uses_user_npm_prefix() → None, call:compile_dockerfile, func:test_compile_dockerfile_starts_as_root_and_drops_privileges() → None, call:compile_dockerfile, call:compile_entrypoint, func:test_compile_compose_runs_as_root() → None, call:compile_compose | dep: pytest, src.services.build.manifest_compiler +- test_manifest_compiler.py | Unit tests for a manifest compiler that generates Dockerfiles, docker-compose files, and entrypoint scripts from manifest configurations. | 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_workspace_not_baked_into_image(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_workspace_name_substituted_in_mount_target(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_removes_stale_placeholder_directory(self) → None, call:compile_entrypoint, method:test_entrypoint_uses_root_then_sudo_for_workspace_symlink(self) → None, call:compile_entrypoint, call:entrypoint.find, 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, func:test_compile_dockerfile_uses_user_npm_prefix() → None, call:compile_dockerfile, func:test_compile_dockerfile_starts_as_root_and_drops_privileges() → None, call:compile_dockerfile, call:compile_entrypoint, func:test_compile_compose_runs_as_root() → None, call:compile_compose | 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 @@ -30,7 +30,7 @@ This package contains comprehensive unit tests for the API application's core se - 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 -Isolated unit testing with heavy mocking of external dependencies (Docker, Git/subprocess, databases, filesystem); tests follow arrange-act-assert pattern covering success paths, edge cases, error handling, and security concerns like path traversal prevention. +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. ## tags test, url, call:notification, git, call:, home, merge, call:db ## symbols diff --git a/apps/api/tests/unit/test_manifest_compiler.py b/apps/api/tests/unit/test_manifest_compiler.py index 1143789..80857ad 100644 --- a/apps/api/tests/unit/test_manifest_compiler.py +++ b/apps/api/tests/unit/test_manifest_compiler.py @@ -270,7 +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 su -l dev -s /bin/bash -c \'exec "$@"\' -- "$@"' in entrypoint + assert 'exec runuser -u dev -- "$@"' in entrypoint @pytest.mark.unit