Adds a new "procedural" tree type for linear step-by-step project workflows (domain controller setup, M365 onboarding, VPN config, etc). Includes intake form builder, two-panel step navigation, variable resolution, procedural exports, 3 seed templates, and UI rename from "Trees" to "Flows". Also archives 19 implemented plan docs and creates deferred features backlog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
31 KiB
Export Improvements Phase B — Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add summary block, custom step markers, detail levels (standard/full), and editable preview modal to the export system.
Architecture: Backend changes to all 4 export generators (markdown, text, HTML, PSA) + schema additions. Frontend changes to ExportPreviewModal (editable textarea) and SessionDetailPage (detail level dropdown, summary toggle). No migration needed.
Tech Stack: Python FastAPI, Pydantic v2, React 19, TypeScript, Tailwind CSS
Prerequisites: Phase A frontend must be merged first (branch feat/export-phase-a). Task 7 depends on maxStepIndex state and handleCopyForTicket from Phase A.
Task 1: Add Phase B Fields to Backend Schema
Files:
- Modify:
backend/app/schemas/session.py:84-91
Step 1: Add include_summary and detail_level to SessionExport
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")
# Phase B
include_summary: bool = False
detail_level: Literal["standard", "full"] = "standard"
Note: Literal is already imported at line 2.
Step 2: Run tests to verify no regressions
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v
Expected: All existing export tests pass (new fields have defaults, so backward-compatible).
Step 3: Commit
git add backend/app/schemas/session.py
git commit -m "feat: add include_summary and detail_level to SessionExport schema"
Task 2: Add Frontend Types for Phase B
Files:
- Modify:
frontend/src/types/session.ts:79-86
Step 1: Add include_summary and detail_level to SessionExport interface
export interface SessionExport {
format: 'text' | 'markdown' | 'html' | 'psa'
include_timestamps?: boolean
include_tree_info?: boolean
include_outcome_notes?: boolean
include_next_steps?: boolean
max_step_index?: number
include_summary?: boolean
detail_level?: 'standard' | 'full'
}
Step 2: Build to verify
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Expected: No type errors.
Step 3: Commit
git add frontend/src/types/session.ts
git commit -m "feat(frontend): add include_summary and detail_level to SessionExport type"
Task 3: Custom Step Markers (B2) in All 4 Generators
Files:
- Modify:
backend/app/services/export_service.py
This is a pure backend change. Detect custom steps by node_id.startswith("custom-") and prefix the step title with [CUSTOM].
Step 1: Update generate_markdown_export (line 163)
Change:
lines.append(f"### Step {i}: {question}")
To:
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
lines.append(f"### Step {i}: {prefix}{question}")
if is_custom:
lines.append("*Custom step added by engineer*")
Step 2: Update generate_text_export (line 250)
Change:
lines.append(f"\n{i}. {question}")
To:
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
lines.append(f"\n{i}. {prefix}{question}")
Step 3: Update generate_html_export (line 342)
Change:
html_parts.append(f'<h3>Step {i}: {question}</h3>')
To:
is_custom = decision.get("node_id", "").startswith("custom-")
custom_badge = '<span style="background: #7c3aed; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.75em; margin-right: 6px;">CUSTOM</span>' if is_custom else ''
html_parts.append(f'<h3>{custom_badge}Step {i}: {question}</h3>')
Step 4: Update generate_psa_export (line 413)
Change:
line = f"{i}. {question}"
To:
is_custom = decision.get("node_id", "").startswith("custom-")
prefix = "[CUSTOM] " if is_custom else ""
line = f"{i}. {prefix}{question}"
Step 5: Run tests
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v
Expected: All pass. (Existing tests don't have custom steps, so markers won't appear — no regressions.)
Step 6: Commit
git add backend/app/services/export_service.py
git commit -m "feat: add [CUSTOM] markers to custom steps in all 4 export generators"
Task 4: Command Output Truncation + Detail Levels (B3)
Files:
- Modify:
backend/app/services/export_service.py
Step 1: Add format-aware _truncate_command_output helper (after _get_command_output at line 91)
The truncation suffix must match the output format — markdown uses *(...)*, text/PSA use plain (...), HTML uses <em>...</em>.
def _truncate_command_output(output: str, max_lines: int = 5, fmt: str = "text") -> str:
"""Truncate command output to max_lines for standard detail level.
Args:
fmt: One of "markdown", "text", "html", "psa" — controls suffix formatting.
"""
lines = output.splitlines()
if len(lines) <= max_lines:
return output
truncated = "\n".join(lines[:max_lines])
count = len(lines)
if fmt == "markdown":
suffix = f"*(full output omitted — {count} lines)*"
elif fmt == "html":
suffix = f"<em>(full output omitted — {count} lines)</em>"
else: # text, psa
suffix = f"(full output omitted — {count} lines)"
return f"{truncated}\n{suffix}"
Step 2: Apply truncation in generate_markdown_export (line 168)
After if command_output := _get_command_output(decision): add:
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="markdown")
Step 3: Apply truncation in generate_text_export (line 255)
After if command_output := _get_command_output(decision): add:
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="text")
Step 4: Apply truncation in generate_html_export (line 347)
After if command_output := _get_command_output(decision): add:
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="html")
Step 5: Apply truncation in generate_psa_export (line 421)
After if command_output := _get_command_output(decision): add:
if options.detail_level == "standard":
command_output = _truncate_command_output(command_output, fmt="psa")
Step 6: Run tests
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v
Expected: All pass (default detail_level="standard" but existing test command outputs are short).
Step 7: Commit
git add backend/app/services/export_service.py
git commit -m "feat: add format-aware command output truncation for standard detail level"
Task 5: Summary Block Generation (B1)
Files:
- Modify:
backend/app/services/export_service.py
Add summary block generation to all 4 generators. The summary is inserted after metadata, before Evidence/Steps. Only rendered when options.include_summary is True.
Design decision — placeholder policy: Empty fields (no outcome_notes, no next_steps) are left blank (empty string), NOT filled with [Edit in preview]. This avoids placeholder text leaking into copied/exported output. The frontend preview textarea is where users add missing info manually.
Step 1: Add _build_summary_fields helper (after _get_outcome_label at line 113)
def _build_summary_fields(session: Session) -> dict[str, str]:
"""Build auto-populated summary fields from session data.
Empty fields are left blank — users fill them in via the editable preview.
"""
tree_name = session.tree_snapshot.get("name", "Troubleshooting Session")
tree_desc = session.tree_snapshot.get("description", "")
issue = f"{tree_name}: {tree_desc}" if tree_desc else tree_name
if session.completed_at:
status = "Resolved" if getattr(session, "outcome", None) == "resolved" else \
f"Completed — {_get_outcome_label(session) or 'Unknown'}"
else:
step_count = len(session.decisions) if session.decisions else 0
status = f"In Progress — paused at step {step_count}" if step_count else "In Progress"
_raw_notes = getattr(session, 'outcome_notes', None)
resolution = (_raw_notes if isinstance(_raw_notes, str) else '').strip()
_raw_next = getattr(session, 'next_steps', None)
next_steps = (_raw_next if isinstance(_raw_next, str) else '').strip()
return {
"issue": issue,
"impact": "",
"status": status,
"resolution": resolution,
"next_steps": next_steps,
}
Step 2: Add _escape_markdown_table helper (right after _build_summary_fields)
Pipe characters and newlines in values break markdown table cells:
def _escape_markdown_table(value: str) -> str:
"""Escape value for use in a markdown table cell."""
return value.replace("|", "\\|").replace("\n", " ")
Step 3: Add summary block to generate_markdown_export
Insert after the tree_info block (after line 138, before the scratchpad section):
if options.include_summary:
summary = _build_summary_fields(session)
esc = _escape_markdown_table
lines.append("## Summary")
lines.append("")
lines.append("| Field | Details |")
lines.append("|-------|---------|")
lines.append(f"| Issue | {esc(summary['issue'])} |")
lines.append(f"| Impact | {esc(summary['impact'])} |")
lines.append(f"| Status | {esc(summary['status'])} |")
lines.append(f"| Resolution | {esc(summary['resolution'])} |")
lines.append(f"| Next Steps | {esc(summary['next_steps'])} |")
lines.append("")
lines.append("---")
lines.append("")
Step 4: Add summary block to generate_text_export
Insert after tree_info block (after line 227, before scratchpad section):
if options.include_summary:
summary = _build_summary_fields(session)
lines.append("SUMMARY")
lines.append("-" * 20)
lines.append(f"Issue: {summary['issue']}")
lines.append(f"Impact: {summary['impact']}")
lines.append(f"Status: {summary['status']}")
lines.append(f"Resolution: {summary['resolution']}")
lines.append(f"Next Steps: {summary['next_steps']}")
lines.append("")
Step 5: Add summary block to generate_html_export
Insert after tree_info </div> (after line 321, before scratchpad section):
if options.include_summary:
summary = _build_summary_fields(session)
html_parts.append('<h2>Summary</h2>')
html_parts.append('<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">')
for label, value in [("Issue", summary["issue"]), ("Impact", summary["impact"]),
("Status", summary["status"]), ("Resolution", summary["resolution"]),
("Next Steps", summary["next_steps"])]:
html_parts.append(f'<tr><td style="padding: 6px 12px; border: 1px solid #ddd; font-weight: bold; width: 120px;">{html.escape(label)}</td>')
html_parts.append(f'<td style="padding: 6px 12px; border: 1px solid #ddd;">{html.escape(value)}</td></tr>')
html_parts.append('</table>')
Step 6: Add summary block to generate_psa_export
Insert after lines.append("") on line 394 (after the header, before PROBLEM section):
if options.include_summary:
summary = _build_summary_fields(session)
lines.append("--- SUMMARY ---")
lines.append(f"Issue: {summary['issue']}")
lines.append(f"Impact: {summary['impact']}")
lines.append(f"Status: {summary['status']}")
lines.append(f"Resolution: {summary['resolution']}")
lines.append(f"Next Steps: {summary['next_steps']}")
lines.append("")
Step 7: Run tests
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py tests/test_export_security.py -v
Expected: All pass (summary is opt-in, existing tests don't set include_summary=True).
Step 8: Commit
git add backend/app/services/export_service.py
git commit -m "feat: add summary block generation to all 4 export generators"
Task 6: Editable Preview Modal (B4)
Files:
- Modify:
frontend/src/components/session/ExportPreviewModal.tsx
Design decision — edit preservation on summary toggle: Toggling "Include Summary" re-fetches from the backend, which resets editedContent. This is documented and expected — the toast says "Summary updated" so the user understands. Edits are lightweight (engineers tweak a few words), so the cost of re-typing is low vs. the complexity of merging diffs.
Step 1: Replace read-only preview with editable textarea and add controls
Rewrite the component:
import { useState, useEffect } from 'react'
import { Copy, Download, Check, RotateCcw } from 'lucide-react'
import { Modal } from '@/components/common/Modal'
import { cn } from '@/lib/utils'
interface ExportPreviewModalProps {
isOpen: boolean
onClose: () => void
content: string
filename: string
format: 'markdown' | 'text' | 'html' | 'psa'
onDownload: (content: string) => void
includeSummary?: boolean
onToggleSummary?: (include: boolean) => void
}
export function ExportPreviewModal({
isOpen,
onClose,
content,
filename,
format,
onDownload,
includeSummary = false,
onToggleSummary,
}: ExportPreviewModalProps) {
const [copied, setCopied] = useState(false)
const [editedContent, setEditedContent] = useState(content)
// Sync editedContent when content prop changes (new fetch / summary toggle)
useEffect(() => {
setEditedContent(content)
}, [content])
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(editedContent)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy:', err)
}
}
const handleDownload = () => {
onDownload(editedContent)
onClose()
}
const handleReset = () => {
setEditedContent(content)
}
const handleClose = () => {
setCopied(false)
onClose()
}
const isModified = editedContent !== content
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Export Preview" size="xl">
{/* Filename, format info, and controls */}
<div className="mb-3 flex flex-wrap items-center justify-between gap-2">
<p className="text-sm text-white/70">
Filename: <span className="font-mono text-white">{filename}</span>
<span className="ml-3 rounded bg-white/10 px-2 py-0.5 text-xs text-white/70">
{format === 'markdown' ? 'Markdown' : format === 'html' ? 'HTML' : format === 'psa' ? 'PSA' : 'Plain Text'}
</span>
{isModified && (
<span className="ml-2 text-xs text-yellow-400">(edited)</span>
)}
</p>
<div className="flex items-center gap-3">
{onToggleSummary && (
<label className="flex items-center gap-2 text-sm text-white/60 cursor-pointer">
<input
type="checkbox"
checked={includeSummary}
onChange={(e) => onToggleSummary(e.target.checked)}
className="h-4 w-4 rounded border-white/20 bg-black/50"
/>
Include Summary
</label>
)}
{isModified && (
<button
onClick={handleReset}
className="flex items-center gap-1 text-xs text-white/40 hover:text-white"
title="Reset to original"
>
<RotateCcw className="h-3 w-3" />
Reset
</button>
)}
</div>
</div>
{/* Editable Content */}
<textarea
value={editedContent}
onChange={(e) => setEditedContent(e.target.value)}
className={cn(
'h-96 w-full resize-y rounded-md border border-white/10 bg-black/50 p-4',
'font-mono text-sm text-white',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
/>
{/* Actions */}
<div className="mt-4 flex items-center justify-end gap-2">
<button
onClick={handleCopy}
className={cn(
'flex items-center gap-2 rounded-md border border-white/10 px-3 py-2 text-sm font-medium',
'text-white/60 hover:bg-white/10 hover:text-white',
'focus:outline-none focus:ring-2 focus:ring-white/20'
)}
>
{copied ? (
<>
<Check className="h-4 w-4 text-emerald-400" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy to Clipboard
</>
)}
</button>
<button
onClick={handleDownload}
className={cn(
'flex items-center gap-2 rounded-md bg-white px-3 py-2 text-sm font-medium text-black',
'hover:bg-white/90 focus:outline-none focus:ring-2 focus:ring-white/20'
)}
>
<Download className="h-4 w-4" />
Download
</button>
</div>
</Modal>
)
}
export default ExportPreviewModal
Key changes:
<pre>→<textarea>witheditedContentlocal state- Copy/Download use
editedContentinstead ofcontentprop - Reset button appears when content is modified
onDownloadsignature changes to(content: string) => voidto receive edited content- New optional
includeSummary+onToggleSummaryprops 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
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Fix any type errors from the onDownload signature change (see Task 7).
Step 3: Commit
git add frontend/src/components/session/ExportPreviewModal.tsx
git commit -m "feat(frontend): convert ExportPreviewModal to editable textarea with reset"
Task 7: Wire Up Phase B Controls in SessionDetailPage
Files:
- Modify:
frontend/src/pages/SessionDetailPage.tsx
Step 1: Add state for detail level and include summary (after line 34)
const [detailLevel, setDetailLevel] = useState<'standard' | 'full'>('standard')
const [includeSummary, setIncludeSummary] = useState(false)
Step 2: Update fetchExportContent to include Phase B options (line 92-101)
const fetchExportContent = async () => {
if (!session) return null
const options: SessionExport = {
format: exportFormat,
include_timestamps: true,
include_tree_info: true,
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
detail_level: detailLevel,
include_summary: includeSummary,
}
return await sessionsApi.export(session.id, options)
}
Step 3: Update handleCopyForTicket to include Phase B options (line 140-145)
const options: SessionExport = {
format: 'psa',
include_timestamps: true,
include_tree_info: true,
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
detail_level: detailLevel,
include_summary: includeSummary,
}
Step 4: Update handleDownload to accept content parameter (line 159)
Change:
const handleDownload = () => {
if (!exportContent || !session) return
const blob = new Blob([exportContent], { type: 'text/plain' })
To:
const handleDownload = (content: string) => {
if (!session) return
const blob = new Blob([content], { type: 'text/plain' })
Step 5: Add onToggleSummary handler with error handling
const handleToggleSummary = async (include: boolean) => {
setIncludeSummary(include)
if (!session) return
const options: SessionExport = {
format: exportFormat,
include_timestamps: true,
include_tree_info: true,
...(maxStepIndex !== null && { max_step_index: maxStepIndex }),
detail_level: detailLevel,
include_summary: include,
}
try {
const content = await sessionsApi.export(session.id, options)
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
}
}
Step 6: Add detail level dropdown to the export controls area (after step cutoff dropdown, before copy button — after line 408)
<select
value={detailLevel}
onChange={(e) => setDetailLevel(e.target.value as 'standard' | 'full')}
aria-label="Detail level"
className={cn(
'rounded-md border border-white/10 bg-black/50 px-3 py-2 text-sm text-white',
'focus:border-white/30 focus:outline-none focus:ring-1 focus:ring-white/20'
)}
>
<option value="standard">Standard</option>
<option value="full">Full Detail</option>
</select>
Step 7: Update ExportPreviewModal props (line 516-523)
<ExportPreviewModal
isOpen={showPreview}
onClose={() => setShowPreview(false)}
content={exportContent || ''}
filename={getFilename()}
format={exportFormat}
onDownload={handleDownload}
includeSummary={includeSummary}
onToggleSummary={handleToggleSummary}
/>
Step 8: Build to verify
Run: cd /c/Dev/Projects/patherly/frontend && npx tsc --noEmit
Expected: No type errors.
Step 9: Commit
git add frontend/src/pages/SessionDetailPage.tsx
git commit -m "feat(frontend): add detail level dropdown and summary toggle to export controls"
Task 8: Backend Tests for Phase B Features
Files:
- Modify:
backend/tests/test_psa_export.py
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
from app.services.export_service import (
generate_psa_export, generate_text_export, generate_markdown_export,
generate_html_export, _format_duration,
)
Step 2: Add TestPhaseB test class at the bottom of the file
class TestPhaseB:
"""Tests for Phase B export features: custom markers, detail levels, summary."""
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</span>" 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 <em> 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 "<em>(full output omitted — 20 lines)</em>" 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: Run all export tests
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest tests/test_psa_export.py -v
Expected: All tests pass including new ones.
Step 4: Commit
git add backend/tests/test_psa_export.py
git commit -m "test: add Phase B tests for custom markers, detail levels, and summary block"
Task 9: Final Build Verification
Step 1: Run full backend tests
Run: cd /c/Dev/Projects/patherly/backend && python -m pytest --override-ini="addopts=" -v
Expected: All tests pass.
Step 2: Run frontend build
Run: cd /c/Dev/Projects/patherly/frontend && npm run build
Expected: Build succeeds with no errors.
Step 3: Verify git status is clean
git status
git log --oneline feat/export-phase-a --not main | head -20
Frontend Acceptance Checklist (Manual QA)
- Editable preview: Open Preview, edit text, verify Copy/Download use edited content. Click Reset to restore original.
- Summary toggle: Check "Include Summary" in preview — export re-fetches with summary block (edits reset, toast confirms). Uncheck removes it.
- Summary toggle error: Disconnect network, toggle summary — checkbox reverts, error toast shown.
- Detail level: Select "Full Detail", export a session with long command output — no truncation. Switch to "Standard" — output truncated with format-appropriate marker.
- Custom step markers: Export a session with custom steps — should show
[CUSTOM]prefix. - 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).
- No placeholder leak: Enable summary on a session with no outcome_notes — Resolution field should be blank, not show
[Edit in preview].