Filename: {filename} @@ -513,6 +541,7 @@ Key changes: - `onDownload` signature changes to `(content: string) => void` to receive edited content - New optional `includeSummary` + `onToggleSummary` props for summary checkbox - `(edited)` indicator when content has been modified +- Summary toggle re-fetches content, resetting edits (documented behavior — avoids diff-merge complexity) **Step 2: Build to verify** @@ -586,12 +615,11 @@ To: const blob = new Blob([content], { type: 'text/plain' }) ``` -**Step 5: Add `onToggleSummary` handler** +**Step 5: Add `onToggleSummary` handler** with error handling ```typescript const handleToggleSummary = async (include: boolean) => { setIncludeSummary(include) - // Re-fetch with new option if (!session) return const options: SessionExport = { format: exportFormat, @@ -603,9 +631,14 @@ To: } try { const content = await sessionsApi.export(session.id, options) - if (content) setExportContent(content) + if (content) { + setExportContent(content) + toast.success(include ? 'Summary added' : 'Summary removed') + } } catch (err) { console.error('Failed to re-fetch export:', err) + toast.error('Failed to update export') + setIncludeSummary(!include) // Revert checkbox on failure } } ``` @@ -659,75 +692,176 @@ git commit -m "feat(frontend): add detail level dropdown and summary toggle to e ## Task 8: Backend Tests for Phase B Features **Files:** -- Modify: `backend/tests/test_export.py` +- Modify: `backend/tests/test_psa_export.py` -**Step 1: Add test for custom step markers** +Uses existing `_make_session` helper and `_default_options` from `test_psa_export.py`. Also imports additional generators for cross-format tests. + +**Step 1: Add imports** at the top of `test_psa_export.py` ```python -def test_custom_step_markers_psa(self, completed_session): - """Custom steps should have [CUSTOM] prefix in export.""" - # Add a custom step to decisions - completed_session.decisions.append({ - "node_id": "custom-abc123", - "question": "Check Additional Logs", - "answer": "Found error in event log", - "notes": None, - "timestamp": "2024-01-01T12:05:00Z", - }) - options = SessionExport(format="psa") - result = generate_psa_export(completed_session, options) - assert "[CUSTOM] Check Additional Logs" in result +from app.services.export_service import ( + generate_psa_export, generate_text_export, generate_markdown_export, + generate_html_export, _format_duration, +) ``` -**Step 2: Add test for command output truncation** +**Step 2: Add `TestPhaseB` test class** at the bottom of the file ```python -def test_command_output_truncation_standard(self, completed_session): - """Standard detail level truncates long command output.""" - long_output = "\n".join(f"line {i}" for i in range(20)) - completed_session.decisions[0]["command_output"] = long_output - options = SessionExport(format="text", detail_level="standard") - result = generate_text_export(completed_session, options) - assert "*(full output omitted — 20 lines)*" in result +class TestPhaseB: + """Tests for Phase B export features: custom markers, detail levels, summary.""" -def test_command_output_full_detail(self, completed_session): - """Full detail level shows all command output.""" - long_output = "\n".join(f"line {i}" for i in range(20)) - completed_session.decisions[0]["command_output"] = long_output - options = SessionExport(format="text", detail_level="full") - result = generate_text_export(completed_session, options) - assert "*(full output omitted" not in result - assert "line 19" in result + def test_custom_step_markers_psa(self): + """Custom steps should have [CUSTOM] prefix in PSA export.""" + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Check DNS", "answer": "OK"}, + {"node_id": "custom-abc123", "question": "Check Additional Logs", "answer": "Found error"}, + ]) + options = SessionExport(format="psa") + result = generate_psa_export(session, options) + assert "[CUSTOM] Check Additional Logs" in result + assert "[CUSTOM] Check DNS" not in result + + def test_custom_step_markers_markdown(self): + """Custom steps should have [CUSTOM] prefix and subtitle in markdown.""" + session = _make_session(decisions=[ + {"node_id": "custom-xyz", "question": "Manual Check", "answer": "Done"}, + ]) + options = SessionExport(format="markdown") + result = generate_markdown_export(session, options) + assert "[CUSTOM] Manual Check" in result + assert "*Custom step added by engineer*" in result + + def test_custom_step_markers_html(self): + """Custom steps should have purple badge in HTML export.""" + session = _make_session(decisions=[ + {"node_id": "custom-xyz", "question": "Manual Check", "answer": "Done"}, + ]) + options = SessionExport(format="html") + result = generate_html_export(session, options) + assert "CUSTOM" in result + + def test_command_output_truncation_standard(self): + """Standard detail level truncates long command output.""" + long_output = "\n".join(f"line {i}" for i in range(20)) + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Run diagnostics", "answer": "See output", + "command_output": long_output}, + ]) + options = SessionExport(format="text", detail_level="standard") + result = generate_text_export(session, options) + assert "(full output omitted — 20 lines)" in result + assert "line 19" not in result + + def test_command_output_full_detail(self): + """Full detail level shows all command output.""" + long_output = "\n".join(f"line {i}" for i in range(20)) + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Run diagnostics", "answer": "See output", + "command_output": long_output}, + ]) + options = SessionExport(format="text", detail_level="full") + result = generate_text_export(session, options) + assert "(full output omitted" not in result + assert "line 19" in result + + def test_truncation_short_output_unchanged(self): + """Short command output is not truncated even in standard mode.""" + short_output = "line 1\nline 2\nline 3" + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Check", "answer": "OK", + "command_output": short_output}, + ]) + options = SessionExport(format="text", detail_level="standard") + result = generate_text_export(session, options) + assert "(full output omitted" not in result + assert "line 3" in result + + def test_truncation_markdown_format(self): + """Markdown format uses italic truncation marker.""" + long_output = "\n".join(f"line {i}" for i in range(20)) + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Check", "answer": "OK", + "command_output": long_output}, + ]) + options = SessionExport(format="markdown", detail_level="standard") + result = generate_markdown_export(session, options) + assert "*(full output omitted — 20 lines)*" in result + + def test_truncation_html_format(self): + """HTML format uses truncation marker.""" + long_output = "\n".join(f"line {i}" for i in range(20)) + session = _make_session(decisions=[ + {"node_id": "node-1", "question": "Check", "answer": "OK", + "command_output": long_output}, + ]) + options = SessionExport(format="html", detail_level="standard") + result = generate_html_export(session, options) + assert "(full output omitted — 20 lines)" in result + + def test_summary_block_psa(self): + """Summary block appears when include_summary is True.""" + session = _make_session() + options = SessionExport(format="psa", include_summary=True) + result = generate_psa_export(session, options) + assert "--- SUMMARY ---" in result + assert "Issue:" in result + assert "Status:" in result + + def test_no_summary_by_default(self): + """Summary block should not appear by default.""" + session = _make_session() + options = SessionExport(format="psa") + result = generate_psa_export(session, options) + assert "--- SUMMARY ---" not in result + + def test_summary_block_markdown(self): + """Summary block in markdown uses table format.""" + session = _make_session() + options = SessionExport(format="markdown", include_summary=True) + result = generate_markdown_export(session, options) + assert "## Summary" in result + assert "| Issue |" in result + + def test_summary_status_completed(self): + """Completed resolved session shows 'Resolved' status in summary.""" + session = _make_session() + session.outcome = "resolved" + options = SessionExport(format="psa", include_summary=True) + result = generate_psa_export(session, options) + assert "Status: Resolved" in result + + def test_summary_status_in_progress(self): + """In-progress session shows step count in summary status.""" + session = _make_session( + decisions=[{"node_id": "n1", "question": "Step 1", "answer": "Done"}], + completed_at=None, + ) + session.completed_at = None + options = SessionExport(format="psa", include_summary=True) + result = generate_psa_export(session, options) + assert "In Progress — paused at step 1" in result + + def test_summary_empty_fields_no_placeholders(self): + """Empty summary fields should be blank, not show placeholders.""" + session = _make_session() + session.outcome_notes = None + session.next_steps = None + options = SessionExport(format="psa", include_summary=True) + result = generate_psa_export(session, options) + assert "[Edit in preview]" not in result ``` -**Step 3: Add test for summary block** +**Step 3: Run all export tests** -```python -def test_summary_block_psa(self, completed_session): - """Summary block appears when include_summary is True.""" - options = SessionExport(format="psa", include_summary=True) - result = generate_psa_export(completed_session, options) - assert "--- SUMMARY ---" in result - assert "Issue:" in result - assert "Status:" in result - -def test_no_summary_by_default(self, completed_session): - """Summary block should not appear by default.""" - options = SessionExport(format="psa") - result = generate_psa_export(completed_session, options) - assert "--- SUMMARY ---" not in result -``` - -**Step 4: Run all export tests** - -Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_export.py -v` +Run: `cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py -v` Expected: All tests pass including new ones. -**Step 5: Commit** +**Step 4: Commit** ```bash -git add backend/tests/test_export.py -git commit -m "test: add tests for custom step markers, detail levels, and summary block" +git add backend/tests/test_psa_export.py +git commit -m "test: add Phase B tests for custom markers, detail levels, and summary block" ``` --- @@ -756,7 +890,9 @@ git log --oneline feat/export-phase-a --not main | head -20 ## Frontend Acceptance Checklist (Manual QA) 1. **Editable preview:** Open Preview, edit text, verify Copy/Download use edited content. Click Reset to restore original. -2. **Summary toggle:** Check "Include Summary" in preview — export re-fetches with summary block. Uncheck removes it. -3. **Detail level:** Select "Full Detail", export a session with long command output — no truncation. Switch to "Standard" — output truncated. -4. **Custom step markers:** Export a session with custom steps — should show `[CUSTOM]` prefix. -5. **Summary block content:** Summary should auto-populate Issue from tree name, Status from completion state, Resolution from outcome_notes. +2. **Summary toggle:** Check "Include Summary" in preview — export re-fetches with summary block (edits reset, toast confirms). Uncheck removes it. +3. **Summary toggle error:** Disconnect network, toggle summary — checkbox reverts, error toast shown. +4. **Detail level:** Select "Full Detail", export a session with long command output — no truncation. Switch to "Standard" — output truncated with format-appropriate marker. +5. **Custom step markers:** Export a session with custom steps — should show `[CUSTOM]` prefix. +6. **Summary block content:** Summary should auto-populate Issue from tree name, Status from completion state, Resolution from outcome_notes. Empty fields are blank (no placeholder text). +7. **No placeholder leak:** Enable summary on a session with no outcome_notes — Resolution field should be blank, not show `[Edit in preview]`.