feat: add batch_id/target_label to sessions and batch launch endpoint
- Add batch_id (UUID, nullable, indexed) and target_label (String 255, nullable) columns to the Session model - Manual Alembic migration 6e8128ef2aa8 applies both columns - POST /sessions/batch creates one session per target for maintenance flows; rejects empty targets (422) and non-maintenance trees (400) - SessionResponse schema exposes batch_id and target_label - 3 new integration tests, all 540 tests pass Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
92
backend/tests/test_batch_sessions.py
Normal file
92
backend/tests/test_batch_sessions.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Tests for batch session launching (maintenance flows)."""
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
async def _create_maintenance_tree(client, headers):
|
||||
"""Helper: create a published maintenance tree."""
|
||||
resp = await client.post(
|
||||
"/api/v1/trees",
|
||||
json={
|
||||
"name": "Patch RDS Servers",
|
||||
"tree_type": "maintenance",
|
||||
"tree_structure": {
|
||||
"steps": [
|
||||
{"id": "s1", "type": "procedure_step", "title": "Install patch",
|
||||
"description": "Run installer", "content_type": "action"},
|
||||
{"id": "end", "type": "procedure_end", "title": "Done"},
|
||||
]
|
||||
},
|
||||
},
|
||||
headers=headers,
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
return resp.json()["id"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_launch_creates_one_session_per_target(client: AsyncClient, auth_headers: dict):
|
||||
tree_id = await _create_maintenance_tree(client, auth_headers)
|
||||
resp = await client.post(
|
||||
"/api/v1/sessions/batch",
|
||||
json={
|
||||
"tree_id": tree_id,
|
||||
"targets": [
|
||||
{"label": "RDS-01", "notes": "192.168.1.10"},
|
||||
{"label": "RDS-02", "notes": "192.168.1.11"},
|
||||
{"label": "RDS-03"},
|
||||
],
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 201, resp.text
|
||||
data = resp.json()
|
||||
assert data["count"] == 3
|
||||
assert data["batch_id"] is not None
|
||||
sessions = data["sessions"]
|
||||
assert len(sessions) == 3
|
||||
# All share the same batch_id
|
||||
batch_ids = {s["batch_id"] for s in sessions}
|
||||
assert len(batch_ids) == 1
|
||||
# Each has a distinct target_label
|
||||
labels = {s["target_label"] for s in sessions}
|
||||
assert labels == {"RDS-01", "RDS-02", "RDS-03"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_launch_rejects_empty_targets(client: AsyncClient, auth_headers: dict):
|
||||
tree_id = await _create_maintenance_tree(client, auth_headers)
|
||||
resp = await client.post(
|
||||
"/api/v1/sessions/batch",
|
||||
json={"tree_id": tree_id, "targets": []},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_batch_launch_rejects_non_maintenance_tree(client: AsyncClient, auth_headers: dict):
|
||||
"""Batch launch only works for maintenance flows."""
|
||||
# Create a procedural tree
|
||||
resp = await client.post(
|
||||
"/api/v1/trees",
|
||||
json={
|
||||
"name": "Regular Project",
|
||||
"tree_type": "procedural",
|
||||
"tree_structure": {
|
||||
"steps": [
|
||||
{"id": "s1", "type": "procedure_step", "title": "Step",
|
||||
"description": "Do it", "content_type": "action"},
|
||||
{"id": "end", "type": "procedure_end", "title": "Done"},
|
||||
]
|
||||
},
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
tree_id = resp.json()["id"]
|
||||
batch_resp = await client.post(
|
||||
"/api/v1/sessions/batch",
|
||||
json={"tree_id": tree_id, "targets": [{"label": "SRV-01"}]},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert batch_resp.status_code == 400
|
||||
Reference in New Issue
Block a user