diff --git a/backend/app/api/endpoints/steps.py b/backend/app/api/endpoints/steps.py index d5c8d279..14bc5a7c 100644 --- a/backend/app/api/endpoints/steps.py +++ b/backend/app/api/endpoints/steps.py @@ -353,6 +353,12 @@ async def update_step( """Update a step (owner or admin only).""" step = await get_step_or_404(step_id, db, current_user, check_edit=True) + if step.is_flow_synced: + raise HTTPException( + status_code=400, + detail="Flow-synced steps are read-only. Fork to customize." + ) + # Validate category if being updated if step_data.category_id: cat_result = await db.execute( diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index dbed1105..ce1ad5ae 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -641,8 +641,8 @@ async def update_tree( if "tree_structure" in update_data: tree.version += 1 - # Sync steps to step library when publishing - if update_data.get("status") == 'published' or tree.status == 'published': + # Sync steps to step library on publish transition only + if update_data.get("status") == 'published': _structure = update_data.get("tree_structure", tree.tree_structure) _type = update_data.get("tree_type", tree.tree_type) _is_public = update_data.get("is_public", tree.is_public) diff --git a/backend/tests/test_step_sync.py b/backend/tests/test_step_sync.py index 8f734f05..63308d64 100644 --- a/backend/tests/test_step_sync.py +++ b/backend/tests/test_step_sync.py @@ -138,3 +138,79 @@ class TestExtractStepsForSync: } results = list(extract_steps_for_sync(tree_structure, tree_type='maintenance')) assert len(results) == 1 + + +class TestSyncOnPublish: + """Integration tests — sync triggered by publishing a tree.""" + + @pytest.mark.asyncio + async def test_publishing_procedural_tree_creates_library_steps( + self, client, auth_headers + ): + # Create a procedural tree with two steps + tree_resp = await client.post("/api/v1/trees", json={ + "name": "Test Procedure", + "tree_type": "procedural", + "status": "draft", + "tree_structure": { + "steps": [ + {"id": "step_1", "type": "procedure_step", + "title": "First step", "description": "Do this first"}, + {"id": "step_2", "type": "procedure_step", + "title": "Second step", "description": "Do this second"}, + {"id": "end_1", "type": "procedure_end", "title": "Done"}, + ] + } + }, headers=auth_headers) + assert tree_resp.status_code == 201 + tree_id = tree_resp.json()["id"] + + # Publish the tree + pub_resp = await client.put(f"/api/v1/trees/{tree_id}", json={"status": "published"}, headers=auth_headers) + assert pub_resp.status_code == 200 + + # Check library has synced entries + lib_resp = await client.get("/api/v1/steps", headers=auth_headers) + assert lib_resp.status_code == 200 + steps = lib_resp.json() + synced = [s for s in steps if s.get("is_flow_synced")] + assert len(synced) == 2 + titles = {s["title"] for s in synced} + assert "First step" in titles + assert "Second step" in titles + + @pytest.mark.asyncio + async def test_republishing_updates_existing_library_steps( + self, client, auth_headers + ): + # Create a draft tree first, then publish + tree_resp = await client.post("/api/v1/trees", json={ + "name": "Update Test", + "tree_type": "procedural", + "status": "draft", + "tree_structure": {"steps": [ + {"id": "step_1", "type": "procedure_step", + "title": "Original title", "description": "Original desc"}, + {"id": "end_1", "type": "procedure_end", "title": "Done"}, + ]} + }, headers=auth_headers) + tree_id = tree_resp.json()["id"] + first_pub = await client.put(f"/api/v1/trees/{tree_id}", json={"status": "published"}, headers=auth_headers) + assert first_pub.status_code == 200 + + # Republish with updated step title + second_pub = await client.put(f"/api/v1/trees/{tree_id}", json={ + "tree_structure": {"steps": [ + {"id": "step_1", "type": "procedure_step", + "title": "Updated title", "description": "Updated desc"}, + {"id": "end_1", "type": "procedure_end", "title": "Done"}, + ]}, + "status": "published" + }, headers=auth_headers) + assert second_pub.status_code == 200 + + # Check library shows updated title (not a duplicate) + lib_resp = await client.get("/api/v1/steps", headers=auth_headers) + synced = [s for s in lib_resp.json() if s.get("is_flow_synced")] + assert len(synced) == 1 + assert synced[0]["title"] == "Updated title" diff --git a/frontend/src/components/procedural-editor/StepEditor.tsx b/frontend/src/components/procedural-editor/StepEditor.tsx index 6ce284ac..26225d1c 100644 --- a/frontend/src/components/procedural-editor/StepEditor.tsx +++ b/frontend/src/components/procedural-editor/StepEditor.tsx @@ -255,8 +255,8 @@ export function StepEditor({ step, stepNumber, onUpdate, onCollapse, availableVa - {/* Library Visibility */} -
Controls visibility in the step library. Defaults to the flow's own visibility setting.
-