import json import shutil from pathlib import Path import pytest from fastapi.testclient import TestClient from app import app, PIPELINES_DIR, LOGS_DIR BASE_DIR = Path(__file__).parent.parent @pytest.fixture(autouse=True) def clean_storage(): """Clean storage before and after each test.""" if PIPELINES_DIR.exists(): shutil.rmtree(PIPELINES_DIR) if LOGS_DIR.exists(): shutil.rmtree(LOGS_DIR) PIPELINES_DIR.mkdir(parents=True, exist_ok=True) LOGS_DIR.mkdir(parents=True, exist_ok=True) yield if PIPELINES_DIR.exists(): shutil.rmtree(PIPELINES_DIR) if LOGS_DIR.exists(): shutil.rmtree(LOGS_DIR) def make_pipeline(name="test", prompt_count=1): return { "id": "test-pipeline-id", "name": name, "prompts": [ { "id": f"prompt-{i}", "text": f"test prompt {i}", "title": f"Test Prompt {i}", "order": i } for i in range(prompt_count) ], "created_at": "2024-01-01T00:00:00+00:00", "updated_at": "2024-01-01T00:00:00+00:00" } @pytest.fixture def client(): return TestClient(app) class TestListPipelines: def test_empty(self, client): resp = client.get("/api/pipelines") assert resp.status_code == 200 assert resp.json() == [] def test_with_pipelines(self, client): p = make_pipeline() client.post("/api/pipelines", json=p) resp = client.get("/api/pipelines") assert resp.status_code == 200 data = resp.json() assert len(data) == 1 assert data[0]["name"] == "test" class TestCreatePipeline: def test_create(self, client): p = make_pipeline() resp = client.post("/api/pipelines", json=p) assert resp.status_code == 200 data = resp.json() assert data["name"] == "test" assert data["id"] == "test-pipeline-id" def test_duplicate_id(self, client): p = make_pipeline() client.post("/api/pipelines", json=p) resp = client.post("/api/pipelines", json=p) assert resp.status_code == 409 class TestDeletePipeline: def test_delete(self, client): p = make_pipeline() client.post("/api/pipelines", json=p) resp = client.delete("/api/pipelines/test-pipeline-id") assert resp.status_code == 200 assert resp.json()["deleted"] == "test-pipeline-id" def test_delete_not_found(self, client): resp = client.delete("/api/pipelines/nonexistent") assert resp.status_code == 404 class TestExecutePipeline: def test_not_found(self, client): resp = client.post("/api/pipelines/nonexistent/execute") assert resp.status_code == 404 def test_execute(self, client, monkeypatch): """Test pipeline execution by patching opencode with echo.""" import asyncio class FakeStream: async def readline(self): return b"" class FakeProcess: returncode = 0 async def wait(self): return 0 async def fake_create_subprocess_exec(*args, **kwargs): proc = FakeProcess() proc.stdout = FakeStream() proc.stderr = FakeStream() return proc monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec) p = make_pipeline() client.post("/api/pipelines", json=p) resp = client.post("/api/pipelines/test-pipeline-id/execute") assert resp.status_code == 200 data = resp.json() assert "run_id" in data assert "log_file" in data def test_log_file_created(self, client, monkeypatch): """Test that log file is created after execution.""" import asyncio class FakeStream: async def readline(self): return b"" class FakeProcess: returncode = 0 async def wait(self): return 0 async def fake_create_subprocess_exec(*args, **kwargs): proc = FakeProcess() proc.stdout = FakeStream() proc.stderr = FakeStream() return proc monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec) p = make_pipeline() client.post("/api/pipelines", json=p) resp = client.post("/api/pipelines/test-pipeline-id/execute") data = resp.json() log_path = Path(data["log_file"]) assert log_path.exists() content = log_path.read_text() assert "Starting pipeline" in content assert "Pipeline finished" in content def test_error_step_stops_pipeline(self, client, monkeypatch): """Test that pipeline stops on first error.""" import asyncio class FakeStream: async def readline(self): return b"" class FakeProcess: returncode = 1 async def wait(self): return 1 async def fake_create_subprocess_exec(*args, **kwargs): proc = FakeProcess() proc.stdout = FakeStream() proc.stderr = FakeStream() return proc monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec) p = make_pipeline(prompt_count=3) client.post("/api/pipelines", json=p) resp = client.post("/api/pipelines/test-pipeline-id/execute") data = resp.json() log_path = Path(data["log_file"]) content = log_path.read_text() assert "Pipeline failed at step 1" in content assert "Step 2" not in content