feat: initial commit - pipeline runner with real-time streaming
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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
|
||||
Reference in New Issue
Block a user