Compare commits
2 Commits
2df37b671b
...
2ee2887eb9
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ee2887eb9 | |||
| efce42b967 |
@@ -39,6 +39,11 @@ als privilegierter Containerprozess, startet Aider aber als separaten Benutzer `
|
||||
Dieser Unterprozess erhaelt nur Modell-Credentials, nicht die Failure-DB- oder
|
||||
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
|
||||
des Hosts wird nicht eingebunden. Abgebrochene Claims werden nach standardmaessig 90
|
||||
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
|
||||
cp .env.example .env
|
||||
# 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 run --rm automation check
|
||||
docker compose run --rm automation once
|
||||
|
||||
@@ -23,6 +23,13 @@ services:
|
||||
POLL_SECONDS: ${POLL_SECONDS:-60}
|
||||
STALE_CLAIM_MINUTES: ${STALE_CLAIM_MINUTES:-90}
|
||||
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:
|
||||
- host.docker.internal:host-gateway
|
||||
volumes:
|
||||
|
||||
@@ -33,6 +33,13 @@ class Settings:
|
||||
agent_command: str
|
||||
agent_model: 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
|
||||
max_changed_files: int
|
||||
max_changed_lines: int
|
||||
@@ -71,6 +78,13 @@ class Settings:
|
||||
"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")),
|
||||
max_changed_files=int(os.getenv("MAX_CHANGED_FILES", "12")),
|
||||
max_changed_lines=int(os.getenv("MAX_CHANGED_LINES", "800")),
|
||||
@@ -87,3 +101,5 @@ class Settings:
|
||||
raise ValueError(f"{self.mode} requires GITEA_TOKEN")
|
||||
if not self.database_password:
|
||||
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 shutil
|
||||
import subprocess
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
@@ -46,8 +47,9 @@ class GitWorkspace:
|
||||
command: str,
|
||||
check: bool = True,
|
||||
username: str | None = None,
|
||||
extra_environment: Mapping[str, str] | None = None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
environment = None
|
||||
environment = dict(os.environ)
|
||||
drop_privileges = None
|
||||
if username:
|
||||
account = pwd.getpwnam(username)
|
||||
@@ -61,6 +63,8 @@ class GitWorkspace:
|
||||
os.setgid(account.pw_gid)
|
||||
os.setuid(account.pw_uid)
|
||||
|
||||
environment.update(extra_environment or {})
|
||||
|
||||
return subprocess.run(
|
||||
shlex.split(command),
|
||||
cwd=self.path,
|
||||
|
||||
@@ -12,6 +12,8 @@ class FailureReport:
|
||||
source: str
|
||||
category: str
|
||||
user_feedback: str | None
|
||||
feedback_type: str | None
|
||||
expected_behavior: str | None
|
||||
prompt: str | None
|
||||
response: str | None
|
||||
sql_query: str | None
|
||||
@@ -29,6 +31,8 @@ class FailureReport:
|
||||
source=row["source"],
|
||||
category=row["category"],
|
||||
user_feedback=row.get("user_feedback"),
|
||||
feedback_type=row.get("feedback_type"),
|
||||
expected_behavior=row.get("expected_behavior"),
|
||||
prompt=row.get("prompt"),
|
||||
response=row.get("response"),
|
||||
sql_query=row.get("sql_query"),
|
||||
|
||||
@@ -29,6 +29,8 @@ def redact(value: str | None) -> str:
|
||||
def build_task(report: FailureReport) -> str:
|
||||
prompt = redact(report.prompt)
|
||||
feedback = redact(report.user_feedback)
|
||||
expected_behavior = redact(report.expected_behavior)
|
||||
response = redact(report.response)
|
||||
error = redact(report.error_message)
|
||||
sql = redact(report.sql_query)
|
||||
|
||||
@@ -44,6 +46,7 @@ Rules:
|
||||
- Do not commit or push; the orchestrator owns Git publication.
|
||||
|
||||
Failure category: {report.category}
|
||||
Feedback type: {report.feedback_type or 'not specified'}
|
||||
Source: {report.source}
|
||||
Model: {redact(str(report.context.get('model', 'unknown')))}
|
||||
Database type: {redact(str(report.context.get('database_type', 'unknown')))}
|
||||
@@ -54,6 +57,12 @@ User request:
|
||||
User feedback:
|
||||
{feedback or '[not available]'}
|
||||
|
||||
Expected behavior:
|
||||
{expected_behavior or '[not available]'}
|
||||
|
||||
Observed answer:
|
||||
{response or '[not available]'}
|
||||
|
||||
Observed error:
|
||||
{error or '[not available]'}
|
||||
|
||||
|
||||
@@ -99,8 +99,35 @@ class AutomationWorker:
|
||||
|
||||
def _run_tests(self, workspace: GitWorkspace) -> list[dict]:
|
||||
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:
|
||||
result = workspace.run(command, check=False, username="coder")
|
||||
result = workspace.run(
|
||||
command,
|
||||
check=False,
|
||||
username="coder",
|
||||
extra_environment=test_environment,
|
||||
)
|
||||
results.append({
|
||||
"command": command,
|
||||
"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-auto-commits" in command
|
||||
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_model": "mistral/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,
|
||||
"max_changed_files": 12,
|
||||
"max_changed_lines": 800,
|
||||
|
||||
@@ -19,8 +19,10 @@ def test_task_contains_constraints_and_redacted_report() -> None:
|
||||
source="user_feedback",
|
||||
category="incorrect_answer",
|
||||
user_feedback="password=secret-value",
|
||||
feedback_type="wrong_join",
|
||||
expected_behavior="Join FK_Artikel with FK_AuftragPos and filter Auftragkennung = 3.",
|
||||
prompt="List locations for admin@example.com",
|
||||
response=None,
|
||||
response="The wrong product count was returned.",
|
||||
sql_query="SELECT * FROM company",
|
||||
error_message=None,
|
||||
context={"model": "mistral", "database_type": "customer"},
|
||||
@@ -31,6 +33,9 @@ def test_task_contains_constraints_and_redacted_report() -> None:
|
||||
assert "admin@example.com" not in task
|
||||
assert "Do not commit or push" in task
|
||||
assert "never mock or stub the behavior being tested" in task
|
||||
assert "Feedback type: wrong_join" in task
|
||||
assert "Join FK_Artikel with FK_AuftragPos and filter Auftragkennung = 3." in task
|
||||
assert "The wrong product count was returned." in task
|
||||
|
||||
|
||||
def test_redact_removes_extended_credentials() -> None:
|
||||
|
||||
@@ -13,6 +13,8 @@ def test_dry_run_generates_task_without_agent_or_git() -> None:
|
||||
source="automatic",
|
||||
category="sql_execution",
|
||||
user_feedback=None,
|
||||
feedback_type=None,
|
||||
expected_behavior=None,
|
||||
prompt="List locations",
|
||||
response=None,
|
||||
sql_query=None,
|
||||
@@ -35,3 +37,20 @@ def test_stop_signal_interrupts_worker_wait() -> None:
|
||||
worker._stop(15, None)
|
||||
|
||||
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