Spaces:
Running
Running
Commit Β·
aad7124
1
Parent(s): 034c2ac
Deploy 2026-01-26 07:56:26
Browse files
Dockerfile
CHANGED
|
@@ -19,8 +19,8 @@ RUN pip install --no-cache-dir uv
|
|
| 19 |
# -------------------------------------------------------------------
|
| 20 |
FROM base AS builder
|
| 21 |
|
| 22 |
-
# Copy
|
| 23 |
-
COPY pyproject.toml uv.lock ./
|
| 24 |
|
| 25 |
# Install dependencies to system (no venv needed in container)
|
| 26 |
RUN uv pip install --system .
|
|
@@ -37,6 +37,9 @@ COPY --from=builder /usr/local/bin /usr/local/bin
|
|
| 37 |
# Copy application source (includes pre-built frontend in src/flow/ui/ui/)
|
| 38 |
COPY src/ ./src/
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
# Install the app itself (editable, uses already-installed deps)
|
| 41 |
RUN uv pip install --system --no-deps -e .
|
| 42 |
|
|
|
|
| 19 |
# -------------------------------------------------------------------
|
| 20 |
FROM base AS builder
|
| 21 |
|
| 22 |
+
# Copy files needed for build (hatchling requires README.md)
|
| 23 |
+
COPY pyproject.toml uv.lock README.md ./
|
| 24 |
|
| 25 |
# Install dependencies to system (no venv needed in container)
|
| 26 |
RUN uv pip install --system .
|
|
|
|
| 37 |
# Copy application source (includes pre-built frontend in src/flow/ui/ui/)
|
| 38 |
COPY src/ ./src/
|
| 39 |
|
| 40 |
+
# Copy files needed for package install
|
| 41 |
+
COPY pyproject.toml README.md ./
|
| 42 |
+
|
| 43 |
# Install the app itself (editable, uses already-installed deps)
|
| 44 |
RUN uv pip install --system --no-deps -e .
|
| 45 |
|
src/flow/ui/services/optimizer_service.py
CHANGED
|
@@ -26,10 +26,14 @@ class OptimizerService:
|
|
| 26 |
|
| 27 |
async def run_job(self, job_id: str | UUID) -> AsyncGenerator[JobProgress, None]:
|
| 28 |
"""Run an optimization job and yield progress updates."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
async with async_session() as session:
|
| 30 |
# Load job
|
| 31 |
result = await session.execute(
|
| 32 |
-
select(OptimizationJob).where(OptimizationJob.id ==
|
| 33 |
)
|
| 34 |
job = result.scalar_one_or_none()
|
| 35 |
if not job:
|
|
@@ -182,7 +186,7 @@ class OptimizerService:
|
|
| 182 |
configs = []
|
| 183 |
for config_id in config_ids:
|
| 184 |
result = await session.execute(
|
| 185 |
-
select(AgentConfig).where(AgentConfig.id == config_id)
|
| 186 |
)
|
| 187 |
db_config = result.scalar_one_or_none()
|
| 188 |
if db_config:
|
|
@@ -207,7 +211,7 @@ class OptimizerService:
|
|
| 207 |
tasks = []
|
| 208 |
for task_id in task_ids:
|
| 209 |
result = await session.execute(
|
| 210 |
-
select(TaskModel).where(TaskModel.id == task_id)
|
| 211 |
)
|
| 212 |
db_task = result.scalar_one_or_none()
|
| 213 |
if db_task:
|
|
|
|
| 26 |
|
| 27 |
async def run_job(self, job_id: str | UUID) -> AsyncGenerator[JobProgress, None]:
|
| 28 |
"""Run an optimization job and yield progress updates."""
|
| 29 |
+
# Convert to UUID if string
|
| 30 |
+
if isinstance(job_id, str):
|
| 31 |
+
job_id = UUID(job_id)
|
| 32 |
+
|
| 33 |
async with async_session() as session:
|
| 34 |
# Load job
|
| 35 |
result = await session.execute(
|
| 36 |
+
select(OptimizationJob).where(OptimizationJob.id == job_id)
|
| 37 |
)
|
| 38 |
job = result.scalar_one_or_none()
|
| 39 |
if not job:
|
|
|
|
| 186 |
configs = []
|
| 187 |
for config_id in config_ids:
|
| 188 |
result = await session.execute(
|
| 189 |
+
select(AgentConfig).where(AgentConfig.id == UUID(config_id))
|
| 190 |
)
|
| 191 |
db_config = result.scalar_one_or_none()
|
| 192 |
if db_config:
|
|
|
|
| 211 |
tasks = []
|
| 212 |
for task_id in task_ids:
|
| 213 |
result = await session.execute(
|
| 214 |
+
select(TaskModel).where(TaskModel.id == UUID(task_id))
|
| 215 |
)
|
| 216 |
db_task = result.scalar_one_or_none()
|
| 217 |
if db_task:
|
src/flow/ui/tests/test_e2e_user_journey.py
CHANGED
|
@@ -447,6 +447,84 @@ class TestAPIEndpoints:
|
|
| 447 |
assert resp.status_code == 200
|
| 448 |
assert resp.json() == []
|
| 449 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
|
| 451 |
if __name__ == "__main__":
|
| 452 |
# Run tests directly
|
|
|
|
| 447 |
assert resp.status_code == 200
|
| 448 |
assert resp.json() == []
|
| 449 |
|
| 450 |
+
@pytest.mark.asyncio
|
| 451 |
+
async def test_job_start_endpoint(self, client: httpx.AsyncClient):
|
| 452 |
+
"""Test that job start endpoint works and updates job status.
|
| 453 |
+
|
| 454 |
+
Note: The actual optimization will likely fail in test environment due to
|
| 455 |
+
missing dependencies, but we verify the endpoint is accessible and
|
| 456 |
+
the job status transitions correctly.
|
| 457 |
+
"""
|
| 458 |
+
print("\n[Job Start Endpoint Test]")
|
| 459 |
+
|
| 460 |
+
# Create config
|
| 461 |
+
config_resp = await client.post(
|
| 462 |
+
"/api/configs",
|
| 463 |
+
json={"name": "start-test-config"},
|
| 464 |
+
)
|
| 465 |
+
assert config_resp.status_code == 201
|
| 466 |
+
config = config_resp.json()
|
| 467 |
+
print(f" β Created config: {config['id'][:8]}...")
|
| 468 |
+
|
| 469 |
+
# Create task
|
| 470 |
+
task_resp = await client.post(
|
| 471 |
+
"/api/tasks",
|
| 472 |
+
json={"name": "start-test-task", "prompt": "Print hello world"},
|
| 473 |
+
)
|
| 474 |
+
assert task_resp.status_code == 201
|
| 475 |
+
task = task_resp.json()
|
| 476 |
+
print(f" β Created task: {task['id'][:8]}...")
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
# Create job
|
| 480 |
+
job_resp = await client.post(
|
| 481 |
+
"/api/jobs",
|
| 482 |
+
json={
|
| 483 |
+
"name": "start-test-job",
|
| 484 |
+
"config_ids": [config["id"]],
|
| 485 |
+
"task_ids": [task["id"]],
|
| 486 |
+
"parallel": 1,
|
| 487 |
+
},
|
| 488 |
+
)
|
| 489 |
+
assert job_resp.status_code == 201
|
| 490 |
+
job = job_resp.json()
|
| 491 |
+
job_id = job["id"]
|
| 492 |
+
print(f" β Created job: {job_id[:8]}...")
|
| 493 |
+
assert job["status"] == "pending"
|
| 494 |
+
print(f" β Job status is 'pending'")
|
| 495 |
+
|
| 496 |
+
# Try to start job - this is a streaming endpoint
|
| 497 |
+
# We just verify it's accessible (200 OK) even if it errors during execution
|
| 498 |
+
print(f" β Attempting to start job...")
|
| 499 |
+
start_resp = await client.post(f"/api/jobs/{job_id}/start")
|
| 500 |
+
# The endpoint should return 200 OK for the SSE stream
|
| 501 |
+
assert start_resp.status_code == 200, f"Start endpoint failed: {start_resp.status_code}"
|
| 502 |
+
print(f" β Start endpoint responded with 200")
|
| 503 |
+
|
| 504 |
+
# Check job status after start attempt
|
| 505 |
+
# It should have transitioned from 'pending' (to 'running' or 'failed')
|
| 506 |
+
status_resp = await client.get(f"/api/jobs/{job_id}")
|
| 507 |
+
assert status_resp.status_code == 200
|
| 508 |
+
updated_job = status_resp.json()
|
| 509 |
+
print(f" β Job status after start: {updated_job['status']}")
|
| 510 |
+
|
| 511 |
+
# Job should no longer be pending
|
| 512 |
+
# It's either 'running', 'failed', or 'completed'
|
| 513 |
+
assert updated_job["status"] in ["running", "failed", "completed"], \
|
| 514 |
+
f"Unexpected status: {updated_job['status']}"
|
| 515 |
+
print(f" β Job transitioned from 'pending' to '{updated_job['status']}'")
|
| 516 |
+
|
| 517 |
+
# Cleanup
|
| 518 |
+
await client.delete(f"/api/jobs/{job_id}")
|
| 519 |
+
print(f" β Deleted job")
|
| 520 |
+
|
| 521 |
+
finally:
|
| 522 |
+
await client.delete(f"/api/tasks/{task['id']}")
|
| 523 |
+
await client.delete(f"/api/configs/{config['id']}")
|
| 524 |
+
print(f" β Cleaned up config and task")
|
| 525 |
+
|
| 526 |
+
print("\n Job start endpoint test passed!")
|
| 527 |
+
|
| 528 |
|
| 529 |
if __name__ == "__main__":
|
| 530 |
# Run tests directly
|