feat: initial commit - pipeline runner with real-time streaming

This commit is contained in:
2026-07-03 09:31:45 +03:00
commit 5a29468b7e
9 changed files with 1000 additions and 0 deletions
+189
View File
@@ -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