feat: initial commit
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = REPO_ROOT / "examples" / "fixtures" / "acme-app"
|
||||
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
sys.path.insert(0, str(REPO_ROOT / "llm")) # atrapa serwera OpenAI
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def hermetyczne_srodowisko(monkeypatch):
|
||||
"""Testy nie mogą zależeć od lokalnego .env developera.
|
||||
|
||||
Taskfile ładuje `.env`, więc bez tego wynik testów zmieniałby się w zależności
|
||||
od tego, jaki endpoint albo budżet ktoś ustawił sobie na swojej maszynie.
|
||||
"""
|
||||
for key in list(os.environ):
|
||||
if key.startswith("CODEMOD_"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def repo_root() -> Path:
|
||||
return REPO_ROOT
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def acme_app(tmp_path: Path) -> Path:
|
||||
"""Kopia fixture'u - testy nigdy nie modyfikują wzorca w repozytorium."""
|
||||
target = tmp_path / "acme-app"
|
||||
shutil.copytree(FIXTURE, target, ignore=shutil.ignore_patterns("__pycache__", ".pytest_cache"))
|
||||
return target
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def settings(tmp_path: Path):
|
||||
from agentic_codemod.config import Settings
|
||||
|
||||
config = Settings.from_env()
|
||||
config.apm_root = REPO_ROOT
|
||||
config.run_dir = tmp_path / "run"
|
||||
config.verify_timeout_s = 120
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def audit(tmp_path: Path):
|
||||
from agentic_codemod.observability.audit import AuditLog
|
||||
|
||||
return AuditLog(run_dir=tmp_path / "audit")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Adaptery ekosystemów - wiedza deterministyczna, więc testowana punktowo."""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from agentic_codemod.adapters import (
|
||||
EcosystemNotDetected,
|
||||
MavenAdapter,
|
||||
NodeNpmAdapter,
|
||||
PythonPipAdapter,
|
||||
detect_adapter,
|
||||
)
|
||||
|
||||
|
||||
def test_wykrywanie_ekosystemu_python(acme_app):
|
||||
assert isinstance(detect_adapter(acme_app), PythonPipAdapter)
|
||||
|
||||
|
||||
def test_brak_rozpoznanego_ekosystemu_to_twardy_blad(tmp_path):
|
||||
with pytest.raises(EcosystemNotDetected):
|
||||
detect_adapter(tmp_path)
|
||||
|
||||
|
||||
def test_odczyt_i_podniesienie_wersji(acme_app):
|
||||
adapter = PythonPipAdapter(acme_app)
|
||||
assert adapter.read_declared_version("acme-sdk") == "==1.4.2"
|
||||
changed = adapter.set_declared_version("acme-sdk", "2.1.0")
|
||||
assert [p.name for p in changed] == ["pyproject.toml"]
|
||||
assert adapter.read_declared_version("acme-sdk") == "==2.1.0"
|
||||
|
||||
|
||||
def test_bump_nie_dotyka_prozy_zawierajacej_nazwe_pakietu(acme_app):
|
||||
"""Regresja: wzorzec bez wymaganego operatora podmieniał też opis projektu."""
|
||||
adapter = PythonPipAdapter(acme_app)
|
||||
adapter.set_declared_version("acme-sdk", "2.1.0")
|
||||
content = (acme_app / "pyproject.toml").read_text(encoding="utf-8")
|
||||
assert "acme-sdk 1.x - fixture" in content
|
||||
|
||||
|
||||
def test_bump_zachowuje_operator_porownania(tmp_path):
|
||||
(tmp_path / "requirements.txt").write_text("acme-sdk >= 1.4\nhttpx==0.27.0\n", encoding="utf-8")
|
||||
adapter = PythonPipAdapter(tmp_path)
|
||||
adapter.set_declared_version("acme-sdk", "2.1.0")
|
||||
assert (tmp_path / "requirements.txt").read_text(encoding="utf-8").startswith("acme-sdk >= 2.1.0")
|
||||
|
||||
|
||||
def test_adapter_maven_podnosi_wersje_zaleznosci(tmp_path):
|
||||
(tmp_path / "pom.xml").write_text(
|
||||
"<project><dependencies><dependency>"
|
||||
"<groupId>com.acme</groupId><artifactId>acme-sdk</artifactId><version>1.4.2</version>"
|
||||
"</dependency></dependencies></project>",
|
||||
encoding="utf-8",
|
||||
)
|
||||
adapter = MavenAdapter(tmp_path)
|
||||
assert adapter.read_declared_version("com.acme:acme-sdk") == "1.4.2"
|
||||
adapter.set_declared_version("com.acme:acme-sdk", "2.1.0")
|
||||
assert adapter.read_declared_version("com.acme:acme-sdk") == "2.1.0"
|
||||
|
||||
|
||||
def test_adapter_npm_zachowuje_prefiks_zakresu(tmp_path):
|
||||
(tmp_path / "package.json").write_text(json.dumps({"dependencies": {"acme-sdk": "^1.4.2"}}), encoding="utf-8")
|
||||
adapter = NodeNpmAdapter(tmp_path)
|
||||
adapter.set_declared_version("acme-sdk", "2.1.0")
|
||||
assert adapter.read_declared_version("acme-sdk") == "^2.1.0"
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Kompilacja definicji APM do obiektów agno.
|
||||
|
||||
Test nie woła modelu - sprawdza to, co da się sprawdzić bez GPU: czy każda definicja
|
||||
agenta z pakietu APM daje się zbudować, czy jej narzędzia istnieją, czy skille się walidują
|
||||
i czy schemat wyjścia jest znany. To bramka na literówkę w `.agent.md`.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
from agno.models.base import Model
|
||||
|
||||
from agentic_codemod.apm.agno_bridge import ToolResolutionError, build_agent, build_skills
|
||||
from agentic_codemod.apm.loader import load_apm_context
|
||||
from agentic_codemod.observability.audit import AuditLog
|
||||
from agentic_codemod.tools import WorkspaceTools, build_tool_registry
|
||||
from agentic_codemod.workflow.brains import SCHEMA_REGISTRY
|
||||
|
||||
|
||||
@dataclass
|
||||
class _StubModel(Model):
|
||||
"""Model, który nigdy nie zostanie wywołany - potrzebny tylko po to,
|
||||
|
||||
by agno przyjęło konstrukcję agenta bez dostępu do endpointu.
|
||||
"""
|
||||
|
||||
id: str = "stub"
|
||||
name: str = "Stub"
|
||||
provider: str = "Stub"
|
||||
|
||||
def invoke(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
async def ainvoke(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def invoke_stream(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
async def ainvoke_stream(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse_provider_response(self, response, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse_provider_response_delta(self, response):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class _StubModelFactory:
|
||||
"""Zastępuje realny endpoint - budowa agenta nie może wymagać dostępnego LLM."""
|
||||
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def for_profile(self, profile, temperature=None):
|
||||
self.calls.append((profile, temperature))
|
||||
return _StubModel(id=f"stub-{profile}")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def registry(acme_app, settings, tmp_path):
|
||||
workspace = WorkspaceTools(acme_app, settings, AuditLog(run_dir=tmp_path / "audit"))
|
||||
return build_tool_registry(workspace)
|
||||
|
||||
|
||||
def test_kazda_definicja_agenta_daje_sie_zbudowac(repo_root, settings, registry):
|
||||
ctx = load_apm_context(repo_root)
|
||||
factory = _StubModelFactory()
|
||||
registry = dict(registry)
|
||||
registry["run_verification"] = lambda: "ok"
|
||||
|
||||
for name, definition in ctx.agents.items():
|
||||
agent = build_agent(definition, ctx, settings, factory, registry, SCHEMA_REGISTRY)
|
||||
assert agent.name == name
|
||||
assert agent.instructions, "agent bez instrukcji to agent bez kontekstu"
|
||||
assert agent.tool_call_limit and agent.tool_call_limit > 0
|
||||
|
||||
assert {profile for profile, _ in factory.calls} <= {"planner", "coder", "reviewer", "scribe"}
|
||||
|
||||
|
||||
def test_schemat_wyjscia_jest_wiazany_z_rejestru(repo_root, settings, registry):
|
||||
ctx = load_apm_context(repo_root)
|
||||
registry = dict(registry)
|
||||
registry["run_verification"] = lambda: "ok"
|
||||
agent = build_agent(ctx.agent("planner"), ctx, settings, _StubModelFactory(), registry, SCHEMA_REGISTRY)
|
||||
assert agent.output_schema is SCHEMA_REGISTRY["ChangePlan"]
|
||||
|
||||
|
||||
def test_nieznane_narzedzie_w_definicji_konczy_sie_bledem(repo_root, settings, registry):
|
||||
ctx = load_apm_context(repo_root)
|
||||
definition = ctx.agent("coder")
|
||||
definition.meta["tools"] = ["read_file", "rm_rf"]
|
||||
with pytest.raises(ToolResolutionError, match="rm_rf"):
|
||||
build_agent(definition, ctx, settings, _StubModelFactory(), registry, SCHEMA_REGISTRY)
|
||||
|
||||
|
||||
def test_skille_agenta_sa_ladowane_z_katalogow_apm(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
skills = build_skills(ctx, ctx.agent("coder").skill_names)
|
||||
assert set(skills.get_skill_names()) == {"safe-code-edit", "sdk-version-upgrade"}
|
||||
assert skills.get_skill("sdk-version-upgrade").references, "notatka migracyjna musi być widoczna dla agenta"
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Kontekst APM jest konfiguracją sieci agentowej - musi się ładować i walidować."""
|
||||
|
||||
import pytest
|
||||
|
||||
from agentic_codemod.apm.loader import load_apm_context
|
||||
from agentic_codemod.apm.primitives import parse_frontmatter
|
||||
|
||||
|
||||
def test_kontekst_zawiera_wszystkie_role_agentow(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
assert {"scout", "planner", "coder", "reviewer", "scribe"} <= set(ctx.agents)
|
||||
assert {"repo-recon", "sdk-version-upgrade", "safe-code-edit", "merge-request-authoring"} <= set(ctx.skills)
|
||||
assert {"security-guardrails", "python-conventions"} <= set(ctx.instructions)
|
||||
|
||||
|
||||
def test_definicja_agenta_wskazuje_istniejace_skille_i_instrukcje(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
for name, definition in ctx.agents.items():
|
||||
assert ctx.skill_paths(definition.skill_names) or not definition.skill_names, name
|
||||
assert ctx.instruction_bodies(definition.instruction_names) or not definition.instruction_names, name
|
||||
|
||||
|
||||
def test_skille_sa_zgodne_ze_specyfikacja_agent_skills(repo_root):
|
||||
"""agno waliduje katalogi skilli - niepoprawny skill musi wysadzić przebieg na starcie."""
|
||||
from agno.skills import LocalSkills
|
||||
|
||||
ctx = load_apm_context(repo_root)
|
||||
for ref in ctx.skills.values():
|
||||
assert LocalSkills(str(ref.path), validate=True).load()
|
||||
|
||||
|
||||
def test_prompt_renderuje_sie_z_wymaganymi_wejsciami(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
prompt = ctx.prompt("sdk-upgrade")
|
||||
rendered = prompt.render({"package": "acme-sdk", "to_version": "2.1.0", "repo": "/tmp/x"})
|
||||
assert "acme-sdk" in rendered and "2.1.0" in rendered
|
||||
assert "{{" not in rendered
|
||||
|
||||
|
||||
def test_brak_wymaganego_wejscia_to_blad_konfiguracji_nie_zgadywanie(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
with pytest.raises(ValueError, match="brak wymaganych wejść"):
|
||||
ctx.prompt("sdk-upgrade").render({"package": "acme-sdk"})
|
||||
|
||||
|
||||
def test_brakujacy_skill_konczy_sie_czytelnym_bledem(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
with pytest.raises(KeyError, match="apm install"):
|
||||
ctx.skill_paths(["nie-istnieje"])
|
||||
|
||||
|
||||
def test_parsowanie_frontmattera_bez_naglowka():
|
||||
meta, body = parse_frontmatter("zwykły tekst")
|
||||
assert meta == {} and body == "zwykły tekst"
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Ślad audytowy jest artefaktem CI - nie może wynosić sekretów."""
|
||||
|
||||
from agentic_codemod.observability.audit import redact
|
||||
|
||||
|
||||
def test_redakcja_tokenow():
|
||||
assert "glpat-" not in redact("token: glpat-ABCDEFGHIJKLMNOP")
|
||||
assert "supersecret" not in redact('api_key="supersecret123"')
|
||||
assert "***REDACTED***" in redact("Authorization: Bearer abcdefghijklmnop")
|
||||
|
||||
|
||||
def test_redakcja_nie_niszczy_zwyklego_tekstu():
|
||||
text = "Zmieniono plik src/acme_app/notifier.py, 9 podmian."
|
||||
assert redact(text) == text
|
||||
|
||||
|
||||
def test_log_zapisuje_wpisy_do_pliku(audit):
|
||||
audit.record("read_file", {"path": "a.py"}, detail="ok")
|
||||
audit.event("intake", package="acme-sdk")
|
||||
assert (audit.run_dir / "trace.jsonl").read_text(encoding="utf-8").count("\n") == 2
|
||||
assert audit.tool_call_count == 1
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Reguły codemod z pakietu APM - deterministyczna część migracji."""
|
||||
|
||||
from agentic_codemod.apm.loader import load_apm_context
|
||||
from agentic_codemod.workflow.codemod import apply_ruleset, load_rulesets
|
||||
|
||||
|
||||
def _ruleset(repo_root):
|
||||
ctx = load_apm_context(repo_root)
|
||||
rulesets = load_rulesets([ref.path for ref in ctx.skills.values()], "acme-sdk")
|
||||
assert rulesets, "pakiet APM musi dostarczać reguły dla acme-sdk"
|
||||
return rulesets[0]
|
||||
|
||||
|
||||
def test_reguly_sa_ladowane_z_referencji_skilla(repo_root):
|
||||
ruleset = _ruleset(repo_root)
|
||||
assert ruleset.package == "acme-sdk"
|
||||
assert {rule.id for rule in ruleset.rules} >= {"import-client", "constructor", "send-message"}
|
||||
|
||||
|
||||
def test_dry_run_nie_zmienia_plikow(repo_root, acme_app):
|
||||
before = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
result = apply_ruleset(acme_app, _ruleset(repo_root), dry_run=True)
|
||||
assert result.changed_files == ["src/acme_app/notifier.py"]
|
||||
assert (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_migracja_usuwa_stare_api(repo_root, acme_app):
|
||||
apply_ruleset(acme_app, _ruleset(repo_root))
|
||||
content = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
assert "AcmeClient(api_key=api_key, base_url=endpoint)" in content
|
||||
assert "client.messages.create(recipient=" in content
|
||||
assert "client.send(" not in content
|
||||
assert ".close()" not in content
|
||||
assert 'result["id"]' not in content
|
||||
|
||||
|
||||
def test_reguly_nie_dotykaja_vendorowanego_sdk(repo_root, acme_app):
|
||||
before = (acme_app / "stubs/acme/__init__.py").read_text(encoding="utf-8")
|
||||
apply_ruleset(acme_app, _ruleset(repo_root))
|
||||
assert (acme_app / "stubs/acme/__init__.py").read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_ponowne_uruchomienie_jest_idempotentne(repo_root, acme_app):
|
||||
ruleset = _ruleset(repo_root)
|
||||
apply_ruleset(acme_app, ruleset)
|
||||
after_first = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
second = apply_ruleset(acme_app, ruleset)
|
||||
assert second.changed_files == []
|
||||
assert (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8") == after_first
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Ścieżka agentowa (LlmBrain) przepuszczona przez atrapę serwera OpenAI.
|
||||
|
||||
Tryb offline sprawdza reguły; ten test sprawdza to, czego reguły nie dotykają:
|
||||
budowę agentów z definicji APM, wywoływanie sandboxowanych narzędzi przez model
|
||||
i parsowanie strukturalnych wyjść. Bez GPU, bez sieci, tak samo na Linuksie i macOS.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from mock_server import serve
|
||||
|
||||
from agentic_codemod.schemas import ChangeRequest, TaskType
|
||||
from agentic_codemod.workflow.runner import run_pipeline
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_llm():
|
||||
httpd = serve(host="127.0.0.1", port=0)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
host, port = httpd.server_address[:2]
|
||||
try:
|
||||
yield f"http://{host}:{port}/v1"
|
||||
finally:
|
||||
httpd.shutdown()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def llm_settings(settings, mock_llm):
|
||||
settings.provider = "openai_like"
|
||||
settings.base_url = mock_llm
|
||||
settings.api_key = "mock"
|
||||
for role in ("planner", "coder", "reviewer", "scribe"):
|
||||
setattr(settings.models, role, "mock/agentic-codemod")
|
||||
return settings
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def request_upgrade(acme_app):
|
||||
return ChangeRequest(
|
||||
task_type=TaskType.SDK_UPGRADE,
|
||||
repo_path=str(acme_app),
|
||||
package="acme-sdk",
|
||||
module_name="acme",
|
||||
from_version="1.4.2",
|
||||
to_version="2.1.0",
|
||||
)
|
||||
|
||||
|
||||
def test_przebieg_z_agentami_konczy_sie_zielona_weryfikacja(request_upgrade, llm_settings, acme_app):
|
||||
manifest = run_pipeline(request_upgrade, settings=llm_settings, mode="llm", in_place=True)
|
||||
|
||||
assert manifest.status == "success", manifest.errors
|
||||
assert manifest.mode == "llm"
|
||||
assert manifest.verifications and manifest.verifications[-1].passed
|
||||
assert set(manifest.changed_files) == {"pyproject.toml", "src/acme_app/notifier.py"}
|
||||
|
||||
migrated = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
assert "AcmeClient(api_key=api_key, base_url=endpoint)" in migrated
|
||||
assert "client.send(" not in migrated
|
||||
|
||||
|
||||
def test_strukturalne_wyjscia_agentow_sa_parsowane(request_upgrade, llm_settings):
|
||||
manifest = run_pipeline(request_upgrade, settings=llm_settings, mode="llm", in_place=True)
|
||||
|
||||
assert manifest.profile and manifest.profile.module_name == "acme"
|
||||
assert manifest.plan and manifest.plan.actionable_edits[0].path == "src/acme_app/notifier.py"
|
||||
assert manifest.review and manifest.review.verdict == "approve"
|
||||
assert manifest.merge_request and manifest.merge_request.title.startswith("build(deps):")
|
||||
|
||||
|
||||
def test_agent_uzywa_sandboxowanych_narzedzi(request_upgrade, llm_settings):
|
||||
manifest = run_pipeline(request_upgrade, settings=llm_settings, mode="llm", in_place=True)
|
||||
|
||||
used = {entry["tool"] for entry in manifest.tool_calls}
|
||||
assert {"read_file", "replace_in_file", "run_verification"} <= used
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Przebieg end-to-end bez modelu językowego.
|
||||
|
||||
To jest smoke test całego pipeline'u: kontekst APM, adaptery, sandbox, workflow agno,
|
||||
weryfikacja i artefakty audytowe. Uruchamialny na runnerze bez GPU i bez internetu.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from agentic_codemod.schemas import ChangeRequest, TaskType
|
||||
from agentic_codemod.workflow.runner import run_pipeline
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def request_upgrade(acme_app):
|
||||
return ChangeRequest(
|
||||
task_type=TaskType.SDK_UPGRADE,
|
||||
repo_path=str(acme_app),
|
||||
package="acme-sdk",
|
||||
module_name="acme",
|
||||
from_version="1.4.2",
|
||||
to_version="2.1.0",
|
||||
)
|
||||
|
||||
|
||||
def test_fixture_jest_czerwony_przed_migracja(acme_app):
|
||||
import subprocess
|
||||
|
||||
proc = subprocess.run(["python3", "-m", "pytest", "-q"], cwd=acme_app, capture_output=True, text=True)
|
||||
assert proc.returncode != 0, "fixture musi być czerwony, inaczej test e2e niczego nie dowodzi"
|
||||
|
||||
|
||||
def test_przebieg_offline_konczy_sie_zielona_weryfikacja(request_upgrade, settings, acme_app):
|
||||
manifest = run_pipeline(request_upgrade, settings=settings, mode="offline", in_place=True)
|
||||
|
||||
assert manifest.status == "success"
|
||||
assert manifest.verifications and manifest.verifications[-1].passed
|
||||
assert set(manifest.changed_files) == {"pyproject.toml", "src/acme_app/notifier.py"}
|
||||
assert manifest.review and manifest.review.verdict == "approve"
|
||||
assert "acme-sdk==2.1.0" in (acme_app / "pyproject.toml").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_przebieg_zapisuje_komplet_artefaktow_audytowych(request_upgrade, settings):
|
||||
run_pipeline(request_upgrade, settings=settings, mode="offline", in_place=True)
|
||||
run_dir = settings.run_dir
|
||||
|
||||
for name in (
|
||||
"run.json",
|
||||
"plan.json",
|
||||
"profile.json",
|
||||
"review.json",
|
||||
"merge_request.md",
|
||||
"changes.patch",
|
||||
"trace.jsonl",
|
||||
):
|
||||
assert (run_dir / name).exists(), f"brak artefaktu {name}"
|
||||
|
||||
manifest = json.loads((run_dir / "run.json").read_text(encoding="utf-8"))
|
||||
assert manifest["apm_context"]["skills"], "manifest musi odnotować, z jakiego kontekstu korzystał przebieg"
|
||||
assert manifest["run_id"] and manifest["mode"] == "offline"
|
||||
|
||||
|
||||
def test_tryb_plan_only_nie_zmienia_kodu(request_upgrade, settings, acme_app):
|
||||
before = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
manifest = run_pipeline(request_upgrade, settings=settings, mode="offline", in_place=True, plan_only=True)
|
||||
|
||||
assert manifest.status == "success"
|
||||
assert manifest.plan is not None and manifest.plan.edits
|
||||
assert manifest.changed_files == []
|
||||
assert (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_praca_na_kopii_nie_rusza_katalogu_zrodlowego(request_upgrade, settings, acme_app):
|
||||
before = (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8")
|
||||
manifest = run_pipeline(request_upgrade, settings=settings, mode="offline", in_place=False)
|
||||
|
||||
assert manifest.status == "success"
|
||||
assert (acme_app / "src/acme_app/notifier.py").read_text(encoding="utf-8") == before
|
||||
|
||||
|
||||
def test_recenzja_blokuje_gdy_weryfikacja_czerwona(request_upgrade, settings, acme_app):
|
||||
"""Sabotujemy testy fixture'u: pipeline nie może zaraportować sukcesu."""
|
||||
(acme_app / "tests" / "test_notifier.py").write_text(
|
||||
"def test_zawsze_czerwony():\n assert False\n", encoding="utf-8"
|
||||
)
|
||||
settings.max_iterations = 1
|
||||
manifest = run_pipeline(request_upgrade, settings=settings, mode="offline", in_place=True)
|
||||
|
||||
assert manifest.status == "failed"
|
||||
assert not manifest.verifications[-1].passed
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Sandbox narzędzi plikowych to ostatnia linia obrony - testujemy go jak zabezpieczenie, nie jak feature."""
|
||||
|
||||
import pytest
|
||||
|
||||
from agentic_codemod.tools import WorkspaceTools
|
||||
from agentic_codemod.tools.workspace import WorkspaceError
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def workspace(acme_app, settings, audit):
|
||||
return WorkspaceTools(acme_app, settings, audit)
|
||||
|
||||
|
||||
def test_odczyt_pliku_zwraca_numerowane_linie(workspace):
|
||||
content = workspace.read_file("src/acme_app/notifier.py")
|
||||
assert " 1| " in content
|
||||
|
||||
|
||||
def test_wyjscie_poza_repozytorium_jest_zabronione(workspace):
|
||||
with pytest.raises(WorkspaceError, match="poza repozytorium"):
|
||||
workspace.read_file("../../etc/passwd")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", [".gitlab-ci.yml", ".git/config", "deploy/secret.env", "app.pem"])
|
||||
def test_sciezki_objete_zakazem_sa_blokowane(workspace, path):
|
||||
with pytest.raises(WorkspaceError, match="zakaz|poza repozytorium"):
|
||||
workspace.write_file(path, "cokolwiek")
|
||||
|
||||
|
||||
def test_replace_wymaga_jednoznacznego_fragmentu(workspace):
|
||||
with pytest.raises(WorkspaceError, match="występuje 2 razy"):
|
||||
workspace.replace_in_file(
|
||||
"src/acme_app/notifier.py",
|
||||
" client = Client(api_key=api_key, endpoint=endpoint)\n",
|
||||
" client = AcmeClient()\n",
|
||||
)
|
||||
|
||||
|
||||
def test_replace_bez_dopasowania_daje_wskazowke_nie_wyjatek_techniczny(workspace):
|
||||
with pytest.raises(WorkspaceError, match="Odczytaj plik ponownie"):
|
||||
workspace.replace_in_file("src/acme_app/notifier.py", "czegoś takiego tu nie ma", "x")
|
||||
|
||||
|
||||
def test_udana_edycja_jest_rejestrowana(workspace):
|
||||
workspace.replace_in_file("src/acme_app/notifier.py", "from acme import Client", "from acme import AcmeClient")
|
||||
assert "src/acme_app/notifier.py" in workspace.changed_files
|
||||
assert any(entry["tool"] == "replace_in_file" and entry["ok"] for entry in workspace.audit.entries)
|
||||
|
||||
|
||||
def test_budzet_zmienionych_plikow_jest_egzekwowany(workspace, settings):
|
||||
settings.max_files_changed = 1
|
||||
workspace.write_file("a.py", "x = 1\n")
|
||||
with pytest.raises(WorkspaceError, match="budżet"):
|
||||
workspace.write_file("b.py", "y = 2\n")
|
||||
|
||||
|
||||
def test_wyszukiwanie_zwraca_dopasowania(workspace):
|
||||
assert "notifier.py" in workspace.search_repo("from acme import", "*.py")
|
||||
Reference in New Issue
Block a user