Isolate database credentials for agent tests
This commit is contained in:
@@ -39,6 +39,11 @@ als privilegierter Containerprozess, startet Aider aber als separaten Benutzer `
|
|||||||
Dieser Unterprozess erhaelt nur Modell-Credentials, nicht die Failure-DB- oder
|
Dieser Unterprozess erhaelt nur Modell-Credentials, nicht die Failure-DB- oder
|
||||||
Gitea-Zugangsdaten.
|
Gitea-Zugangsdaten.
|
||||||
|
|
||||||
|
Die nachgelagerten Testprozesse erhalten separat konfigurierte `TEST_DB_*`-Zugangsdaten.
|
||||||
|
Dieser Benutzer darf ausschliesslich die isolierten Testschemas bearbeiten. Fuer Laravel
|
||||||
|
werden daraus `DB_*`, `ERP_DB_*` und `COCKPIT_CONFIG_DATABASE` gesetzt; Aider selbst sieht
|
||||||
|
diese Werte nie. Produktive ERP- oder Kundenzugangsdaten duerfen hier nicht verwendet werden.
|
||||||
|
|
||||||
Laravel-Tests laufen mit PHP und Composer direkt im isolierten Worker. Der Docker-Socket
|
Laravel-Tests laufen mit PHP und Composer direkt im isolierten Worker. Der Docker-Socket
|
||||||
des Hosts wird nicht eingebunden. Abgebrochene Claims werden nach standardmaessig 90
|
des Hosts wird nicht eingebunden. Abgebrochene Claims werden nach standardmaessig 90
|
||||||
Minuten wieder freigegeben; `STALE_CLAIM_MINUTES` passt dieses Intervall an.
|
Minuten wieder freigegeben; `STALE_CLAIM_MINUTES` passt dieses Intervall an.
|
||||||
@@ -48,6 +53,7 @@ Minuten wieder freigegeben; `STALE_CLAIM_MINUTES` passt dieses Intervall an.
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# FAILURE_DB_PASSWORD und bei Bedarf MISTRAL_API_KEY eintragen
|
# FAILURE_DB_PASSWORD und bei Bedarf MISTRAL_API_KEY eintragen
|
||||||
|
# TEST_DB_PASSWORD fuer den Benutzer der isolierten Testschemas eintragen
|
||||||
docker compose build
|
docker compose build
|
||||||
docker compose run --rm automation check
|
docker compose run --rm automation check
|
||||||
docker compose run --rm automation once
|
docker compose run --rm automation once
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ services:
|
|||||||
POLL_SECONDS: ${POLL_SECONDS:-60}
|
POLL_SECONDS: ${POLL_SECONDS:-60}
|
||||||
STALE_CLAIM_MINUTES: ${STALE_CLAIM_MINUTES:-90}
|
STALE_CLAIM_MINUTES: ${STALE_CLAIM_MINUTES:-90}
|
||||||
TEST_COMMANDS: ${TEST_COMMANDS:-composer install --no-interaction --prefer-dist;;php artisan test}
|
TEST_COMMANDS: ${TEST_COMMANDS:-composer install --no-interaction --prefer-dist;;php artisan test}
|
||||||
|
TEST_DB_HOST: ${TEST_DB_HOST:-host.docker.internal}
|
||||||
|
TEST_DB_PORT: ${TEST_DB_PORT:-33060}
|
||||||
|
TEST_DB_NAME: ${TEST_DB_NAME:-laravel_ai_chat_agent_test}
|
||||||
|
TEST_DB_USER: ${TEST_DB_USER:-lai_test_runner}
|
||||||
|
TEST_DB_PASSWORD: ${TEST_DB_PASSWORD:-agent_test_secret}
|
||||||
|
TEST_CUSTOMER_DB: ${TEST_CUSTOMER_DB:-customer_agent_test}
|
||||||
|
TEST_CONFIG_DB: ${TEST_CONFIG_DB:-cockpit_agent_test}
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- host.docker.internal:host-gateway
|
- host.docker.internal:host-gateway
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ class Settings:
|
|||||||
agent_command: str
|
agent_command: str
|
||||||
agent_model: str
|
agent_model: str
|
||||||
test_commands: tuple[str, ...]
|
test_commands: tuple[str, ...]
|
||||||
|
test_database_host: str
|
||||||
|
test_database_port: int
|
||||||
|
test_database_name: str
|
||||||
|
test_database_user: str
|
||||||
|
test_database_password: str
|
||||||
|
test_customer_database: str
|
||||||
|
test_config_database: str
|
||||||
poll_seconds: int
|
poll_seconds: int
|
||||||
max_changed_files: int
|
max_changed_files: int
|
||||||
max_changed_lines: int
|
max_changed_lines: int
|
||||||
@@ -71,6 +78,13 @@ class Settings:
|
|||||||
"composer install --no-interaction --prefer-dist;;php artisan test",
|
"composer install --no-interaction --prefer-dist;;php artisan test",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
test_database_host=os.getenv("TEST_DB_HOST", "host.docker.internal"),
|
||||||
|
test_database_port=int(os.getenv("TEST_DB_PORT", "33060")),
|
||||||
|
test_database_name=os.getenv("TEST_DB_NAME", "laravel_ai_chat_agent_test"),
|
||||||
|
test_database_user=os.getenv("TEST_DB_USER", "lai_test_runner"),
|
||||||
|
test_database_password=os.getenv("TEST_DB_PASSWORD", ""),
|
||||||
|
test_customer_database=os.getenv("TEST_CUSTOMER_DB", "customer_agent_test"),
|
||||||
|
test_config_database=os.getenv("TEST_CONFIG_DB", "cockpit_agent_test"),
|
||||||
poll_seconds=int(os.getenv("POLL_SECONDS", "60")),
|
poll_seconds=int(os.getenv("POLL_SECONDS", "60")),
|
||||||
max_changed_files=int(os.getenv("MAX_CHANGED_FILES", "12")),
|
max_changed_files=int(os.getenv("MAX_CHANGED_FILES", "12")),
|
||||||
max_changed_lines=int(os.getenv("MAX_CHANGED_LINES", "800")),
|
max_changed_lines=int(os.getenv("MAX_CHANGED_LINES", "800")),
|
||||||
@@ -87,3 +101,5 @@ class Settings:
|
|||||||
raise ValueError(f"{self.mode} requires GITEA_TOKEN")
|
raise ValueError(f"{self.mode} requires GITEA_TOKEN")
|
||||||
if not self.database_password:
|
if not self.database_password:
|
||||||
raise ValueError("FAILURE_DB_PASSWORD is required")
|
raise ValueError("FAILURE_DB_PASSWORD is required")
|
||||||
|
if not self.test_database_password:
|
||||||
|
raise ValueError("TEST_DB_PASSWORD is required")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import pwd
|
|||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -46,8 +47,9 @@ class GitWorkspace:
|
|||||||
command: str,
|
command: str,
|
||||||
check: bool = True,
|
check: bool = True,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
|
extra_environment: Mapping[str, str] | None = None,
|
||||||
) -> subprocess.CompletedProcess[str]:
|
) -> subprocess.CompletedProcess[str]:
|
||||||
environment = None
|
environment = dict(os.environ)
|
||||||
drop_privileges = None
|
drop_privileges = None
|
||||||
if username:
|
if username:
|
||||||
account = pwd.getpwnam(username)
|
account = pwd.getpwnam(username)
|
||||||
@@ -61,6 +63,8 @@ class GitWorkspace:
|
|||||||
os.setgid(account.pw_gid)
|
os.setgid(account.pw_gid)
|
||||||
os.setuid(account.pw_uid)
|
os.setuid(account.pw_uid)
|
||||||
|
|
||||||
|
environment.update(extra_environment or {})
|
||||||
|
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
shlex.split(command),
|
shlex.split(command),
|
||||||
cwd=self.path,
|
cwd=self.path,
|
||||||
|
|||||||
@@ -99,8 +99,35 @@ class AutomationWorker:
|
|||||||
|
|
||||||
def _run_tests(self, workspace: GitWorkspace) -> list[dict]:
|
def _run_tests(self, workspace: GitWorkspace) -> list[dict]:
|
||||||
results = []
|
results = []
|
||||||
|
test_environment = {
|
||||||
|
"AGENT_TEST_DB_ENABLED": "true",
|
||||||
|
"TEST_DB_HOST": self.settings.test_database_host,
|
||||||
|
"TEST_DB_PORT": str(self.settings.test_database_port),
|
||||||
|
"TEST_DB_NAME": self.settings.test_database_name,
|
||||||
|
"TEST_DB_USER": self.settings.test_database_user,
|
||||||
|
"TEST_DB_PASSWORD": self.settings.test_database_password,
|
||||||
|
"TEST_CUSTOMER_DB": self.settings.test_customer_database,
|
||||||
|
"TEST_CONFIG_DB": self.settings.test_config_database,
|
||||||
|
"DB_CONNECTION": "mysql",
|
||||||
|
"DB_HOST": self.settings.test_database_host,
|
||||||
|
"DB_PORT": str(self.settings.test_database_port),
|
||||||
|
"DB_DATABASE": self.settings.test_database_name,
|
||||||
|
"DB_USERNAME": self.settings.test_database_user,
|
||||||
|
"DB_PASSWORD": self.settings.test_database_password,
|
||||||
|
"ERP_DB_HOST": self.settings.test_database_host,
|
||||||
|
"ERP_DB_PORT": str(self.settings.test_database_port),
|
||||||
|
"ERP_DATABASE": self.settings.test_customer_database,
|
||||||
|
"ERP_DB_USERNAME": self.settings.test_database_user,
|
||||||
|
"ERP_DB_PASSWORD": self.settings.test_database_password,
|
||||||
|
"COCKPIT_CONFIG_DATABASE": self.settings.test_config_database,
|
||||||
|
}
|
||||||
for command in self.settings.test_commands:
|
for command in self.settings.test_commands:
|
||||||
result = workspace.run(command, check=False, username="coder")
|
result = workspace.run(
|
||||||
|
command,
|
||||||
|
check=False,
|
||||||
|
username="coder",
|
||||||
|
extra_environment=test_environment,
|
||||||
|
)
|
||||||
results.append({
|
results.append({
|
||||||
"command": command,
|
"command": command,
|
||||||
"returncode": result.returncode,
|
"returncode": result.returncode,
|
||||||
|
|||||||
@@ -12,4 +12,27 @@ def test_aider_command_cannot_mutate_git_configuration_or_send_analytics() -> No
|
|||||||
assert "--no-analytics" in command
|
assert "--no-analytics" in command
|
||||||
assert "--no-auto-commits" in command
|
assert "--no-auto-commits" in command
|
||||||
assert command.count("/dev/null") == 2
|
assert command.count("/dev/null") == 2
|
||||||
assert command[-4:] == ["--edit-format", "diff", "--timeout", "60"]
|
assert command[-4:] == ["--edit-format", "diff", "--timeout", "60"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_aider_environment_does_not_include_database_credentials(tmp_path, monkeypatch) -> None:
|
||||||
|
config = settings(workspace_root=tmp_path)
|
||||||
|
workspace = tmp_path / "repo"
|
||||||
|
(workspace / ".git").mkdir(parents=True)
|
||||||
|
monkeypatch.setenv("TEST_DB_PASSWORD", "must-not-reach-aider")
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def run(command, **kwargs):
|
||||||
|
captured.update(kwargs["env"])
|
||||||
|
return type("Result", (), {"returncode": 0, "stdout": "ok", "stderr": ""})()
|
||||||
|
|
||||||
|
monkeypatch.setattr("lai_automation.agent.subprocess.run", run)
|
||||||
|
monkeypatch.setattr("lai_automation.agent.os.chown", lambda *args, **kwargs: None)
|
||||||
|
monkeypatch.setattr("lai_automation.agent.pwd.getpwnam", lambda _: type(
|
||||||
|
"Account", (), {"pw_dir": str(tmp_path), "pw_uid": 10001, "pw_gid": 10001}
|
||||||
|
)())
|
||||||
|
|
||||||
|
AiderAgent(config).run(workspace, "fix it")
|
||||||
|
|
||||||
|
assert "TEST_DB_PASSWORD" not in captured
|
||||||
|
assert "DB_PASSWORD" not in captured
|
||||||
@@ -25,6 +25,13 @@ def settings(**overrides) -> Settings:
|
|||||||
"agent_command": "aider",
|
"agent_command": "aider",
|
||||||
"agent_model": "mistral/test",
|
"agent_model": "mistral/test",
|
||||||
"test_commands": ("php artisan test",),
|
"test_commands": ("php artisan test",),
|
||||||
|
"test_database_host": "test-db",
|
||||||
|
"test_database_port": 3306,
|
||||||
|
"test_database_name": "lai_agent_test",
|
||||||
|
"test_database_user": "test_runner",
|
||||||
|
"test_database_password": "test-secret",
|
||||||
|
"test_customer_database": "customer_agent_test",
|
||||||
|
"test_config_database": "cockpit_agent_test",
|
||||||
"poll_seconds": 60,
|
"poll_seconds": 60,
|
||||||
"max_changed_files": 12,
|
"max_changed_files": 12,
|
||||||
"max_changed_lines": 800,
|
"max_changed_lines": 800,
|
||||||
|
|||||||
@@ -35,3 +35,20 @@ def test_stop_signal_interrupts_worker_wait() -> None:
|
|||||||
worker._stop(15, None)
|
worker._stop(15, None)
|
||||||
|
|
||||||
assert worker.stop_event.is_set()
|
assert worker.stop_event.is_set()
|
||||||
|
|
||||||
|
|
||||||
|
def test_database_credentials_are_only_passed_to_test_process() -> None:
|
||||||
|
worker = AutomationWorker(settings())
|
||||||
|
workspace = Mock()
|
||||||
|
workspace.run.return_value = Mock(returncode=0, stdout="ok", stderr="")
|
||||||
|
|
||||||
|
results = worker._run_tests(workspace)
|
||||||
|
|
||||||
|
assert results[0]["returncode"] == 0
|
||||||
|
environment = workspace.run.call_args.kwargs["extra_environment"]
|
||||||
|
assert environment["AGENT_TEST_DB_ENABLED"] == "true"
|
||||||
|
assert environment["TEST_DB_USER"] == "test_runner"
|
||||||
|
assert environment["TEST_DB_PASSWORD"] == "test-secret"
|
||||||
|
assert environment["DB_DATABASE"] == "lai_agent_test"
|
||||||
|
assert environment["ERP_DATABASE"] == "customer_agent_test"
|
||||||
|
assert environment["COCKPIT_CONFIG_DATABASE"] == "cockpit_agent_test"
|
||||||
|
|||||||
Reference in New Issue
Block a user