diff --git a/backend/app/api/endpoints/sessions.py b/backend/app/api/endpoints/sessions.py
index 4dbb83c9..68bdaf4c 100644
--- a/backend/app/api/endpoints/sessions.py
+++ b/backend/app/api/endpoints/sessions.py
@@ -314,9 +314,10 @@ async def export_session(
from app.services.variable_service import resolve_variables
content = resolve_variables(content, session_vars)
- # Mark as exported
- session.exported = True
- await db.commit()
+ # Only mark as exported if session is completed
+ if session.completed_at:
+ session.exported = True
+ await db.commit()
return PlainTextResponse(content=content, media_type=media_type)
diff --git a/backend/app/services/export_service.py b/backend/app/services/export_service.py
index bcdad10f..ac8f0135 100644
--- a/backend/app/services/export_service.py
+++ b/backend/app/services/export_service.py
@@ -150,7 +150,11 @@ def generate_markdown_export(session: Session, options: SessionExport) -> str:
lines.append("## Troubleshooting Steps")
lines.append("")
- for i, decision in enumerate(session.decisions, 1):
+ decisions = session.decisions
+ if options.max_step_index is not None:
+ decisions = decisions[:options.max_step_index]
+
+ for i, decision in enumerate(decisions, 1):
question = decision.get("question") or decision.get("action_performed", "Step")
answer = decision.get("answer", "")
notes = decision.get("notes", "")
@@ -233,7 +237,11 @@ def generate_text_export(session: Session, options: SessionExport) -> str:
lines.append("TROUBLESHOOTING STEPS")
lines.append("-" * 20)
- for i, decision in enumerate(session.decisions, 1):
+ decisions = session.decisions
+ if options.max_step_index is not None:
+ decisions = decisions[:options.max_step_index]
+
+ for i, decision in enumerate(decisions, 1):
question = decision.get("question") or decision.get("action_performed", "Step")
answer = decision.get("answer", "")
notes = decision.get("notes", "")
@@ -320,7 +328,11 @@ def generate_html_export(session: Session, options: SessionExport) -> str:
html_parts.append('
Troubleshooting Steps
')
- for i, decision in enumerate(session.decisions, 1):
+ decisions = session.decisions
+ if options.max_step_index is not None:
+ decisions = decisions[:options.max_step_index]
+
+ for i, decision in enumerate(decisions, 1):
question = html.escape(decision.get("question") or decision.get("action_performed", "Step"))
answer = html.escape(decision.get("answer", ""))
notes = html.escape(decision.get("notes", ""))
@@ -388,8 +400,11 @@ def generate_psa_export(session: Session, options: SessionExport) -> str:
# Steps taken
lines.append("--- STEPS TAKEN ---")
- if session.decisions:
- for i, decision in enumerate(session.decisions, 1):
+ decisions = session.decisions
+ if options.max_step_index is not None:
+ decisions = decisions[:options.max_step_index]
+ if decisions:
+ for i, decision in enumerate(decisions, 1):
question = decision.get("question") or decision.get("action_performed", "Step")
answer = decision.get("answer", "")
notes = decision.get("notes", "")
diff --git a/backend/tests/test_sessions.py b/backend/tests/test_sessions.py
index b3c49021..4b650738 100644
--- a/backend/tests/test_sessions.py
+++ b/backend/tests/test_sessions.py
@@ -1184,3 +1184,138 @@ class TestSessions:
content = response.text
assert "## Resolution" not in content
assert "Should not appear" not in content
+
+ @pytest.mark.asyncio
+ async def test_export_max_step_index(
+ self, client: AsyncClient, auth_headers: dict, test_tree: dict
+ ):
+ """Test max_step_index limits exported steps."""
+ create_response = await client.post(
+ "/api/v1/sessions",
+ json={"tree_id": test_tree["id"]},
+ headers=auth_headers
+ )
+ session_id = create_response.json()["id"]
+
+ decisions = [
+ {"node_id": "n1", "question": "Step one?", "answer": "Yes", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
+ {"node_id": "n2", "question": "Step two?", "answer": "No", "timestamp": "2026-02-13T10:01:00Z", "attachments": []},
+ {"node_id": "n3", "question": "Step three?", "answer": "Maybe", "timestamp": "2026-02-13T10:02:00Z", "attachments": []},
+ ]
+ await client.put(
+ f"/api/v1/sessions/{session_id}",
+ json={"decisions": decisions},
+ headers=auth_headers
+ )
+
+ response = await client.post(
+ f"/api/v1/sessions/{session_id}/export",
+ json={"format": "markdown", "max_step_index": 2},
+ headers=auth_headers
+ )
+ content = response.text
+ assert "Step one?" in content
+ assert "Step two?" in content
+ assert "Step three?" not in content
+
+ @pytest.mark.asyncio
+ async def test_export_max_step_index_exceeds_count(
+ self, client: AsyncClient, auth_headers: dict, test_tree: dict
+ ):
+ """Test max_step_index larger than decision count returns all steps."""
+ create_response = await client.post(
+ "/api/v1/sessions",
+ json={"tree_id": test_tree["id"]},
+ headers=auth_headers
+ )
+ session_id = create_response.json()["id"]
+
+ decisions = [
+ {"node_id": "n1", "question": "Only step", "answer": "Done", "timestamp": "2026-02-13T10:00:00Z", "attachments": []},
+ ]
+ await client.put(
+ f"/api/v1/sessions/{session_id}",
+ json={"decisions": decisions},
+ headers=auth_headers
+ )
+
+ response = await client.post(
+ f"/api/v1/sessions/{session_id}/export",
+ json={"format": "markdown", "max_step_index": 100},
+ headers=auth_headers
+ )
+ assert response.status_code == 200
+ assert "Only step" in response.text
+
+ @pytest.mark.asyncio
+ async def test_export_max_step_index_zero_returns_422(
+ self, client: AsyncClient, auth_headers: dict, test_tree: dict
+ ):
+ """Test max_step_index=0 returns validation error."""
+ create_response = await client.post(
+ "/api/v1/sessions",
+ json={"tree_id": test_tree["id"]},
+ headers=auth_headers
+ )
+ session_id = create_response.json()["id"]
+
+ response = await client.post(
+ f"/api/v1/sessions/{session_id}/export",
+ json={"format": "markdown", "max_step_index": 0},
+ headers=auth_headers
+ )
+ assert response.status_code == 422
+
+ @pytest.mark.asyncio
+ async def test_export_in_progress_session_does_not_mark_exported(
+ self, client: AsyncClient, auth_headers: dict, test_tree: dict
+ ):
+ """Test that exporting an in-progress session does NOT set exported=True."""
+ create_response = await client.post(
+ "/api/v1/sessions",
+ json={"tree_id": test_tree["id"]},
+ headers=auth_headers
+ )
+ session_id = create_response.json()["id"]
+
+ await client.post(
+ f"/api/v1/sessions/{session_id}/export",
+ json={"format": "markdown"},
+ headers=auth_headers
+ )
+
+ response = await client.get(
+ f"/api/v1/sessions/{session_id}",
+ headers=auth_headers
+ )
+ assert response.json()["exported"] is False
+
+ @pytest.mark.asyncio
+ async def test_export_completed_session_marks_exported(
+ self, client: AsyncClient, auth_headers: dict, test_tree: dict
+ ):
+ """Test that exporting a completed session sets exported=True."""
+ create_response = await client.post(
+ "/api/v1/sessions",
+ json={"tree_id": test_tree["id"]},
+ headers=auth_headers
+ )
+ session_id = create_response.json()["id"]
+
+ await client.post(
+ f"/api/v1/sessions/{session_id}/complete",
+ json={"outcome": "resolved"},
+ headers=auth_headers
+ )
+
+ await client.post(
+ f"/api/v1/sessions/{session_id}/export",
+ json={"format": "markdown"},
+ headers=auth_headers
+ )
+
+ response = await client.get(
+ f"/api/v1/sessions/{session_id}",
+ headers=auth_headers
+ )
+ assert response.json()["exported"] is True