From d75e6f78e102af98733da6c01b0911881312c227 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 17 Feb 2026 10:35:31 -0500 Subject: [PATCH] feat: add maintenance tree_type with db migration and tests - Expand ck_trees_tree_type CHECK constraint to include 'maintenance' - Add 'maintenance' to TreeType Literal in schemas - Treat maintenance trees as procedural in can_publish_tree validation - Alembic migration 0f1ca2af3647 drops and recreates the constraint - Two integration tests: create and filter by tree_type=maintenance Co-Authored-By: Claude Sonnet 4.5 --- .../0f1ca2af3647_add_maintenance_tree_type.py | 33 ++++++++++++ backend/app/core/tree_validation.py | 4 +- backend/app/models/tree.py | 2 +- backend/app/schemas/tree.py | 2 +- backend/tests/test_maintenance_tree_type.py | 52 +++++++++++++++++++ 5 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 backend/alembic/versions/0f1ca2af3647_add_maintenance_tree_type.py create mode 100644 backend/tests/test_maintenance_tree_type.py diff --git a/backend/alembic/versions/0f1ca2af3647_add_maintenance_tree_type.py b/backend/alembic/versions/0f1ca2af3647_add_maintenance_tree_type.py new file mode 100644 index 00000000..dd29ec95 --- /dev/null +++ b/backend/alembic/versions/0f1ca2af3647_add_maintenance_tree_type.py @@ -0,0 +1,33 @@ +"""add maintenance tree type + +Revision ID: 0f1ca2af3647 +Revises: 039 +Create Date: 2026-02-17 10:25:54.959861 + +""" +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = '0f1ca2af3647' +down_revision: Union[str, None] = '039' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("ALTER TABLE trees DROP CONSTRAINT ck_trees_tree_type") + op.execute( + "ALTER TABLE trees ADD CONSTRAINT ck_trees_tree_type " + "CHECK (tree_type IN ('troubleshooting', 'procedural', 'maintenance'))" + ) + + +def downgrade() -> None: + op.execute("UPDATE trees SET tree_type = 'procedural' WHERE tree_type = 'maintenance'") + op.execute("ALTER TABLE trees DROP CONSTRAINT ck_trees_tree_type") + op.execute( + "ALTER TABLE trees ADD CONSTRAINT ck_trees_tree_type " + "CHECK (tree_type IN ('troubleshooting', 'procedural'))" + ) diff --git a/backend/app/core/tree_validation.py b/backend/app/core/tree_validation.py index 8d079a1f..38c729fd 100644 --- a/backend/app/core/tree_validation.py +++ b/backend/app/core/tree_validation.py @@ -224,14 +224,14 @@ def can_publish_tree( errors.append({"field": "name", "message": "Tree must have a name to be published"}) # Validate structure based on tree type - if tree_type == "procedural": + if tree_type in ("procedural", "maintenance"): structure_valid, structure_errors = validate_procedural_structure(tree_structure) else: structure_valid, structure_errors = validate_tree_structure(tree_structure) errors.extend(structure_errors) # Validate intake form if present (procedural only) - if intake_form and tree_type == "procedural": + if intake_form and tree_type in ("procedural", "maintenance"): form_valid, form_errors = _validate_intake_form(intake_form) errors.extend(form_errors) diff --git a/backend/app/models/tree.py b/backend/app/models/tree.py index 49486a21..825e1d6c 100644 --- a/backend/app/models/tree.py +++ b/backend/app/models/tree.py @@ -29,7 +29,7 @@ class Tree(Base): name='ck_trees_status' ), CheckConstraint( - "tree_type IN ('troubleshooting', 'procedural')", + "tree_type IN ('troubleshooting', 'procedural', 'maintenance')", name='ck_trees_tree_type' ), ) diff --git a/backend/app/schemas/tree.py b/backend/app/schemas/tree.py index 236b6290..c19c5f70 100644 --- a/backend/app/schemas/tree.py +++ b/backend/app/schemas/tree.py @@ -7,7 +7,7 @@ import re # --- Tree Type --- -TreeType = Literal['troubleshooting', 'procedural'] +TreeType = Literal['troubleshooting', 'procedural', 'maintenance'] # --- Intake Form Schemas --- diff --git a/backend/tests/test_maintenance_tree_type.py b/backend/tests/test_maintenance_tree_type.py new file mode 100644 index 00000000..1ba33bd2 --- /dev/null +++ b/backend/tests/test_maintenance_tree_type.py @@ -0,0 +1,52 @@ +"""Tests for maintenance tree type.""" +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_create_maintenance_tree(client: AsyncClient, auth_headers: dict): + """Maintenance tree type is accepted by the API.""" + resp = await client.post( + "/api/v1/trees", + json={ + "name": "Update FSLogix", + "description": "Monthly FSLogix update procedure", + "tree_type": "maintenance", + "tree_structure": { + "steps": [ + {"id": "step-1", "type": "procedure_step", "title": "Download installer", + "description": "Get latest FSLogix from Microsoft", "content_type": "action"}, + {"id": "step-end", "type": "procedure_end", "title": "Complete"}, + ] + }, + }, + headers=auth_headers, + ) + assert resp.status_code == 201, resp.text + data = resp.json() + assert data["tree_type"] == "maintenance" + + +@pytest.mark.asyncio +async def test_list_maintenance_trees_filter(client: AsyncClient, auth_headers: dict): + """Filtering by tree_type=maintenance returns only maintenance trees.""" + await client.post( + "/api/v1/trees", + json={ + "name": "Maintenance Only", + "tree_type": "maintenance", + "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, + ) + resp = await client.get("/api/v1/trees?tree_type=maintenance", headers=auth_headers) + assert resp.status_code == 200 + trees = resp.json() + assert all(t["tree_type"] == "maintenance" for t in trees) + assert len(trees) >= 1