754 lines
24 KiB
Markdown
754 lines
24 KiB
Markdown
# Export Improvements Phase A — Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Add outcome_notes, next_steps, mid-session export awareness, and partial step cutoff to all four export formats.
|
|
|
|
**Architecture:** Add `next_steps` column via migration. Extend `SessionExport` schema with new options. Update all four generators in `export_service.py` to render Resolution + Next Steps sections and support step slicing. Guard `exported=True` behind completion check.
|
|
|
|
**Tech Stack:** Python, FastAPI, SQLAlchemy, Alembic, pytest
|
|
|
|
**Spec:** [2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md](2026-02-13-EXPORT-IMPROVEMENTS-SPEC.md) — Phase A only
|
|
|
|
---
|
|
|
|
## Task 1: Add `next_steps` column — Migration + Model
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/models/session.py:56` (after scratchpad)
|
|
- Create: `backend/alembic/versions/034_add_next_steps_to_sessions.py`
|
|
|
|
**Step 1: Add column to Session model**
|
|
|
|
In `backend/app/models/session.py`, add after the `scratchpad` column (line 58):
|
|
|
|
```python
|
|
next_steps: Mapped[Optional[str]] = mapped_column(
|
|
Text, nullable=True, server_default=sa.text("''")
|
|
)
|
|
```
|
|
|
|
**Step 2: Generate migration**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic revision --autogenerate -m "add next_steps to sessions"`
|
|
|
|
Review the generated migration — it should add a single `next_steps` TEXT column with server_default `''`.
|
|
|
|
Rename the file to `034_add_next_steps_to_sessions.py` and update the revision ID comment if needed for clarity.
|
|
|
|
**Step 3: Run migration**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && python -m alembic upgrade head`
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add backend/app/models/session.py backend/alembic/versions/*next_steps*
|
|
git commit -m "feat: add next_steps column to sessions table"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Update Schemas — SessionExport, SessionUpdate, SessionResponse, SessionComplete
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/schemas/session.py`
|
|
|
|
**Step 1: Update SessionExport** (line 82)
|
|
|
|
Replace the current `SessionExport` class with:
|
|
|
|
```python
|
|
class SessionExport(BaseModel):
|
|
format: str = Field(default="markdown", pattern="^(text|markdown|html|psa)$")
|
|
include_timestamps: bool = True
|
|
include_tree_info: bool = True
|
|
# Phase A
|
|
include_outcome_notes: bool = True
|
|
include_next_steps: bool = True
|
|
max_step_index: Optional[int] = Field(None, ge=1, description="1-based inclusive step cutoff")
|
|
```
|
|
|
|
**Step 2: Add next_steps to SessionUpdate** (line 46)
|
|
|
|
Add to `SessionUpdate`:
|
|
|
|
```python
|
|
next_steps: Optional[str] = None
|
|
```
|
|
|
|
**Step 3: Add next_steps to SessionResponse** (line 56)
|
|
|
|
Add after `outcome_notes`:
|
|
|
|
```python
|
|
next_steps: str = ""
|
|
```
|
|
|
|
Update the validator to normalize both fields:
|
|
|
|
```python
|
|
@validator('scratchpad', 'next_steps', pre=True, always=True)
|
|
def normalize_text_fields(cls, v):
|
|
return v or ""
|
|
```
|
|
|
|
Remove the old `normalize_scratchpad` validator.
|
|
|
|
**Step 4: Add next_steps to SessionComplete** (line 88)
|
|
|
|
```python
|
|
class SessionComplete(BaseModel):
|
|
outcome: SessionOutcome
|
|
outcome_notes: Optional[str] = None
|
|
next_steps: Optional[str] = None
|
|
```
|
|
|
|
**Step 5: Run tests to verify no regressions**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
|
|
|
Expected: All existing tests pass (new fields have defaults).
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add backend/app/schemas/session.py
|
|
git commit -m "feat: add next_steps and export options to session schemas"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Update Completion Endpoint to Save next_steps
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/api/endpoints/sessions.py:236-238`
|
|
|
|
**Step 1: Write failing test**
|
|
|
|
In `backend/tests/test_sessions.py`, add after the existing completion tests:
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_complete_session_with_next_steps(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Test completing session saves next_steps."""
|
|
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}/complete",
|
|
json={
|
|
"outcome": "resolved",
|
|
"outcome_notes": "Fixed the issue",
|
|
"next_steps": "Monitor for 48 hours"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["next_steps"] == "Monitor for 48 hours"
|
|
assert data["outcome_notes"] == "Fixed the issue"
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v`
|
|
|
|
Expected: FAIL — `next_steps` not saved (returns `""` because endpoint doesn't set it).
|
|
|
|
**Step 3: Update completion endpoint**
|
|
|
|
In `backend/app/api/endpoints/sessions.py`, line 238, add after `session.outcome_notes = completion_data.outcome_notes`:
|
|
|
|
```python
|
|
session.next_steps = completion_data.next_steps
|
|
```
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_complete_session_with_next_steps -v`
|
|
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
|
git commit -m "feat: save next_steps on session completion"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Update SessionUpdate Endpoint to Save next_steps
|
|
|
|
**Note:** `update_session` blocks updates to completed sessions (returns 400). This is intentional — `next_steps` is set during active sessions or at completion time, not after. No changes needed to that guard.
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/api/endpoints/sessions.py` (the `update_session` handler)
|
|
|
|
**Step 1: Write failing test**
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_update_session_next_steps(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Test updating next_steps via session update."""
|
|
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.put(
|
|
f"/api/v1/sessions/{session_id}",
|
|
json={"next_steps": "Schedule follow-up call"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["next_steps"] == "Schedule follow-up call"
|
|
```
|
|
|
|
**Step 2: Run test — verify it fails**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v`
|
|
|
|
**Step 3: Update the update_session endpoint**
|
|
|
|
Find the `update_session` handler in `sessions.py`. Look for where it applies `SessionUpdate` fields to the session object. Add `next_steps` to that block, following the same pattern as `scratchpad`:
|
|
|
|
```python
|
|
if update_data.next_steps is not None:
|
|
session.next_steps = update_data.next_steps
|
|
```
|
|
|
|
**Step 4: Run test — verify it passes**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py::TestSessions::test_update_session_next_steps -v`
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
|
git commit -m "feat: allow next_steps update via session update endpoint"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Add outcome_notes + next_steps to Export Generators
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/services/export_service.py`
|
|
- Test: `backend/tests/test_sessions.py`
|
|
|
|
This is the core export change. All four generators need Resolution and Next Steps sections after the steps.
|
|
|
|
**Step 1: Write failing tests**
|
|
|
|
Add to `test_sessions.py`:
|
|
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_export_includes_outcome_notes_in_resolution(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Test that outcome_notes appear as Resolution section in exports."""
|
|
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",
|
|
"outcome_notes": "Replaced failed DIMM in slot A2",
|
|
"next_steps": "Monitor for 24 hours"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Test markdown
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "markdown"},
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
content = response.text
|
|
assert "## Resolution" in content
|
|
assert "Replaced failed DIMM in slot A2" in content
|
|
assert "## Next Steps" in content
|
|
assert "Monitor for 24 hours" in content
|
|
|
|
# Test text
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "text"},
|
|
headers=auth_headers
|
|
)
|
|
content = response.text
|
|
assert "RESOLUTION" in content
|
|
assert "Replaced failed DIMM in slot A2" in content
|
|
assert "NEXT STEPS" in content
|
|
assert "Monitor for 24 hours" in content
|
|
|
|
# Test HTML
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "html"},
|
|
headers=auth_headers
|
|
)
|
|
content = response.text
|
|
assert "Resolution" in content
|
|
assert "Replaced failed DIMM in slot A2" in content
|
|
assert "Next Steps" in content
|
|
assert "Monitor for 24 hours" in content
|
|
|
|
# Test PSA
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "psa"},
|
|
headers=auth_headers
|
|
)
|
|
content = response.text
|
|
assert "Replaced failed DIMM in slot A2" in content
|
|
assert "Monitor for 24 hours" in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_omits_empty_resolution_and_next_steps(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Test that empty outcome_notes/next_steps don't create empty sections."""
|
|
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
|
|
)
|
|
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "markdown"},
|
|
headers=auth_headers
|
|
)
|
|
content = response.text
|
|
assert "## Resolution" not in content
|
|
assert "## Next Steps" not in content
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_export_exclude_outcome_notes_flag(
|
|
self, client: AsyncClient, auth_headers: dict, test_tree: dict
|
|
):
|
|
"""Test include_outcome_notes=False suppresses resolution section."""
|
|
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",
|
|
"outcome_notes": "Should not appear"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
|
|
response = await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "markdown", "include_outcome_notes": False},
|
|
headers=auth_headers
|
|
)
|
|
content = response.text
|
|
assert "## Resolution" not in content
|
|
assert "Should not appear" not in content
|
|
```
|
|
|
|
**Step 2: Run tests — verify they fail**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v`
|
|
|
|
**Step 3: Update all four generators in export_service.py**
|
|
|
|
Add Resolution and Next Steps sections after the Troubleshooting Steps in each generator. The generators receive `options: SessionExport` — use `options.include_outcome_notes` and `options.include_next_steps`.
|
|
|
|
**Markdown generator** — add before `return "\n".join(lines)` (line 178):
|
|
|
|
```python
|
|
# Resolution / Outcome Notes
|
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
|
if outcome_notes.strip() and options.include_outcome_notes:
|
|
lines.append("---")
|
|
lines.append("")
|
|
lines.append("## Resolution")
|
|
lines.append("")
|
|
lines.append(outcome_notes.strip())
|
|
lines.append("")
|
|
|
|
# Next Steps
|
|
next_steps = getattr(session, 'next_steps', '') or ''
|
|
if next_steps.strip() and options.include_next_steps:
|
|
lines.append("---")
|
|
lines.append("")
|
|
lines.append("## Next Steps")
|
|
lines.append("")
|
|
lines.append(next_steps.strip())
|
|
lines.append("")
|
|
```
|
|
|
|
**Text generator** — add before `return "\n".join(lines)` (line 235):
|
|
|
|
```python
|
|
# Resolution
|
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
|
if outcome_notes.strip() and options.include_outcome_notes:
|
|
lines.append("")
|
|
lines.append("RESOLUTION")
|
|
lines.append("-" * 20)
|
|
lines.append(outcome_notes.strip())
|
|
|
|
# Next Steps
|
|
next_steps = getattr(session, 'next_steps', '') or ''
|
|
if next_steps.strip() and options.include_next_steps:
|
|
lines.append("")
|
|
lines.append("NEXT STEPS")
|
|
lines.append("-" * 20)
|
|
lines.append(next_steps.strip())
|
|
```
|
|
|
|
**HTML generator** — add before `html_parts.extend(['</body>', '</html>'])` (line 307):
|
|
|
|
```python
|
|
# Resolution
|
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
|
if outcome_notes.strip() and options.include_outcome_notes:
|
|
html_parts.append('<h2>Resolution</h2>')
|
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(outcome_notes.strip())}</div>')
|
|
|
|
# Next Steps
|
|
next_steps = getattr(session, 'next_steps', '') or ''
|
|
if next_steps.strip() and options.include_next_steps:
|
|
html_parts.append('<h2>Next Steps</h2>')
|
|
html_parts.append(f'<div style="white-space: pre-wrap; margin-bottom: 20px;">{html.escape(next_steps.strip())}</div>')
|
|
```
|
|
|
|
**PSA generator** — update the existing `--- RESOLUTION ---` section (lines 363-373) to use `outcome_notes` when available, and add a next steps section:
|
|
|
|
Replace the resolution section with:
|
|
|
|
```python
|
|
# Resolution
|
|
lines.append("--- RESOLUTION ---")
|
|
outcome_notes = getattr(session, 'outcome_notes', '') or ''
|
|
if outcome_notes.strip() and options.include_outcome_notes:
|
|
lines.append(outcome_notes.strip())
|
|
elif session.decisions:
|
|
last_decision = session.decisions[-1]
|
|
resolution = last_decision.get("answer") or last_decision.get("question", "No resolution recorded")
|
|
lines.append(resolution)
|
|
else:
|
|
lines.append("No resolution recorded.")
|
|
if outcome_label:
|
|
lines.append(f"Outcome: {outcome_label}")
|
|
lines.append("")
|
|
|
|
# Next Steps
|
|
next_steps = getattr(session, 'next_steps', '') or ''
|
|
if next_steps.strip() and options.include_next_steps:
|
|
lines.append("--- NEXT STEPS ---")
|
|
lines.append(next_steps.strip())
|
|
lines.append("")
|
|
```
|
|
|
|
**Step 4: Run tests — verify they pass**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "outcome_notes_in_resolution or omits_empty_resolution or exclude_outcome_notes_flag" -v`
|
|
|
|
**Step 5: Run full test suite**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add backend/app/services/export_service.py backend/tests/test_sessions.py
|
|
git commit -m "feat: add Resolution and Next Steps sections to all export formats"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Partial Export with Step Cutoff (max_step_index)
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/services/export_service.py`
|
|
- Test: `backend/tests/test_sessions.py`
|
|
|
|
**Step 1: Write failing tests**
|
|
|
|
```python
|
|
@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"]
|
|
|
|
# Add 3 decisions
|
|
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
|
|
)
|
|
|
|
# Export with cutoff at step 2
|
|
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
|
|
```
|
|
|
|
**Step 2: Run tests — verify they fail** (the 422 test should already pass due to `ge=1` on schema)
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v`
|
|
|
|
**Step 3: Apply step slicing in all four generators**
|
|
|
|
In each generator, right before the `for i, decision in enumerate(session.decisions, 1):` loop, add:
|
|
|
|
```python
|
|
decisions = session.decisions
|
|
if options.max_step_index is not None:
|
|
decisions = decisions[:options.max_step_index]
|
|
```
|
|
|
|
Then change the loop to iterate over `decisions` instead of `session.decisions`.
|
|
|
|
Apply this in:
|
|
- `generate_markdown_export` (line ~153)
|
|
- `generate_text_export` (line ~214)
|
|
- `generate_html_export` (line ~283)
|
|
- `generate_psa_export` (line ~338)
|
|
|
|
**Step 4: Run tests — verify they pass**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "max_step_index" -v`
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add backend/app/services/export_service.py backend/tests/test_sessions.py
|
|
git commit -m "feat: add max_step_index for partial exports"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Mid-Session Export — Don't Set exported=True
|
|
|
|
**Files:**
|
|
- Modify: `backend/app/api/endpoints/sessions.py:316-318`
|
|
- Test: `backend/tests/test_sessions.py`
|
|
|
|
**Step 1: Write failing test**
|
|
|
|
```python
|
|
@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"]
|
|
|
|
# Export without completing
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "markdown"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Check session - should NOT be marked exported
|
|
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"]
|
|
|
|
# Complete first
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/complete",
|
|
json={"outcome": "resolved"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Export
|
|
await client.post(
|
|
f"/api/v1/sessions/{session_id}/export",
|
|
json={"format": "markdown"},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Check - should be marked exported
|
|
response = await client.get(
|
|
f"/api/v1/sessions/{session_id}",
|
|
headers=auth_headers
|
|
)
|
|
assert response.json()["exported"] is True
|
|
```
|
|
|
|
**Step 2: Run tests — verify the first one fails**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v`
|
|
|
|
**Step 3: Update export endpoint**
|
|
|
|
In `backend/app/api/endpoints/sessions.py`, replace lines 316-318:
|
|
|
|
```python
|
|
# Mark as exported
|
|
session.exported = True
|
|
await db.commit()
|
|
```
|
|
|
|
With:
|
|
|
|
```python
|
|
# Only mark as exported if session is completed
|
|
if session.completed_at:
|
|
session.exported = True
|
|
await db.commit()
|
|
```
|
|
|
|
**Step 4: Run tests — verify they pass**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest tests/test_sessions.py -k "in_progress_session_does_not_mark_exported or completed_session_marks_exported" -v`
|
|
|
|
**Step 5: Run full test suite**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -x -q`
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add backend/app/api/endpoints/sessions.py backend/tests/test_sessions.py
|
|
git commit -m "feat: only mark session exported when completed"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Final Verification
|
|
|
|
**Step 1: Run full test suite**
|
|
|
|
Run: `cd /c/Dev/Projects/patherly/backend && pytest --override-ini="addopts=" -v`
|
|
|
|
Expected: All tests pass, including all new tests.
|
|
|
|
**Step 2: Verify backward compatibility**
|
|
|
|
Confirm that the existing export tests (the ones from before our changes) still pass with no modifications — the new schema fields all have defaults.
|
|
|
|
**Step 3: Commit any remaining changes and verify git status is clean**
|
|
|
|
```bash
|
|
git status
|
|
```
|