Skip to content

Commit 99c7abf

Browse files
committed
feat: business/technical view toggle, translate.py, 89 new tests
1 parent 95b00b5 commit 99c7abf

13 files changed

Lines changed: 1138 additions & 111 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
88

99
### Added
1010

11+
- Business/Technical view toggle — switch between plain-English and developer views (persisted in localStorage)
12+
- `translate.py` module — deterministic business-language translations for step descriptions, triggers, secrets, connection types
13+
- Business-mode Mermaid diagrams — 4 pre-rendered flow variants (detailed/compact × tech/business)
14+
- Translated UI labels: "External Service" not "API Call", "Runs daily at midnight" not "0 0 * * *", "AWS credentials" not "AWS_SECRET_ACCESS_KEY"
15+
- Jinja2 template globals for translation functions — eliminates duplicated label dicts
16+
- Per-step error isolation in business mode — one bad translation doesn't kill the whole diagram
1117
- `--from-json` flag for `serve` command — load pre-computed analysis from JSON file
1218
- Dockerfile + docker-compose for public demo deployment (pre-baked LLM summaries)
1319
- Compact mode for script flow diagrams — functions with >8 steps collapse to summary nodes

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
[![Tests](https://github.com/Lexi-Energy/visualpy/actions/workflows/ci.yml/badge.svg)](https://github.com/Lexi-Energy/visualpy/actions/workflows/ci.yml)
55
[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](https://www.python.org/downloads/)
66

7-
Auto-visualise Python automations for non-technical stakeholders.
7+
Auto-visualise Python automations so that even non-dev people can understand what is going on.
8+
(for example when an agentic system created scripts, or a teammember, or you vibe-coded sth. and have no idea how to give feedback now.)
89

910
Drop a folder of Python scripts, get a visual breakdown of what they do, how they connect, and what they need. No execution required, no config needed.
1011

tests/test_mermaid.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,3 +537,85 @@ def test_script_flow_with_hello_fixture(hello_script, fixtures_dir):
537537
assert ":::fileio" in result
538538
assert ":::decision" in result
539539
assert "subgraph" in result # hello.py has functions
540+
541+
542+
# --- business mode ---
543+
544+
545+
def test_step_node_business_uses_translation():
546+
"""business=True produces translated label instead of raw description."""
547+
svc = Service(name="Google Sheets", library="gspread")
548+
step = Step(line_number=10, type="api_call", description="gspread.authorize()", service=svc)
549+
result = _step_node(step, "test", business=True)
550+
assert "Authenticates with Google Sheets" in result
551+
# Should NOT have the technical prefix
552+
assert "API:" not in result
553+
554+
555+
def test_step_node_business_default_false():
556+
"""Default business=False still produces technical output."""
557+
step = Step(line_number=10, type="api_call", description="requests.get()")
558+
result_default = _step_node(step, "test")
559+
result_explicit = _step_node(step, "test", business=False)
560+
assert result_default == result_explicit
561+
assert "API: requests.get()" in result_default
562+
563+
564+
def test_script_flow_business_drops_parens():
565+
"""business=True subgraph labels drop the () suffix."""
566+
steps = [
567+
Step(line_number=1, type="api_call", description="requests.get()", function_name="fetch"),
568+
Step(line_number=2, type="output", description="print()", function_name="fetch"),
569+
]
570+
script = AnalyzedScript(path="test.py", steps=steps)
571+
result = script_flow(script, business=True)
572+
# Business: "fetch" not "fetch()"
573+
assert '"fetch"' in result
574+
assert '"fetch()"' not in result
575+
576+
577+
def test_script_flow_technical_keeps_parens():
578+
"""Default technical mode keeps () on subgraph labels."""
579+
steps = [
580+
Step(line_number=1, type="api_call", description="requests.get()", function_name="fetch"),
581+
]
582+
script = AnalyzedScript(path="test.py", steps=steps)
583+
result = script_flow(script, business=False)
584+
assert '"fetch()"' in result
585+
586+
587+
def test_compact_node_business_labels():
588+
"""business=True uses BUSINESS_LABELS in compact summary and drops ()."""
589+
steps = [
590+
Step(line_number=i, type="file_io", description=f"op{i}")
591+
for i in range(3)
592+
]
593+
result = _compact_function_node("save_data", steps, "test", business=True)
594+
assert "Read/Write File" in result # BUSINESS_LABELS["file_io"]
595+
assert "File I/O" not in result # _TYPE_LABELS["file_io"]
596+
assert "save_data" in result
597+
assert "save_data()" not in result # no parens in business mode
598+
599+
600+
def test_compact_node_technical_labels():
601+
"""Default technical mode uses _TYPE_LABELS and keeps ()."""
602+
steps = [
603+
Step(line_number=i, type="file_io", description=f"op{i}")
604+
for i in range(3)
605+
]
606+
result = _compact_function_node("save_data", steps, "test")
607+
assert "File I/O" in result
608+
assert "save_data()" in result
609+
610+
611+
def test_project_graph_business_connections():
612+
"""business=True translates connection edge labels."""
613+
scripts = [AnalyzedScript(path="a.py"), AnalyzedScript(path="b.py")]
614+
connections = [
615+
ScriptConnection(source="a.py", target="b.py", type="import", detail=""),
616+
]
617+
project = AnalyzedProject(path="/tmp", scripts=scripts, connections=connections)
618+
result_biz = project_graph(project, business=True)
619+
result_tech = project_graph(project, business=False)
620+
assert '"uses"' in result_biz
621+
assert '"import"' in result_tech

tests/test_server.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,3 +480,84 @@ async def test_script_view_with_hello_fixture(hello_script, fixtures_dir):
480480
resp = await ac.get(f"/script/{script.path}")
481481
assert resp.status_code == 200
482482
assert "graph TB" in resp.text
483+
484+
485+
# --- Business mode ---
486+
487+
488+
@pytest.mark.anyio
489+
async def test_script_view_has_four_flows():
490+
"""Response should contain all 4 flow variants (detailed/compact × tech/biz)."""
491+
steps = [
492+
Step(line_number=i, type="api_call", description=f"call{i}", function_name="big")
493+
for i in range(35)
494+
]
495+
script = AnalyzedScript(path="big.py", steps=steps)
496+
project = AnalyzedProject(path="/tmp", scripts=[script])
497+
app = create_app(project)
498+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
499+
resp = await ac.get("/script/big.py")
500+
assert "flow-detailed" in resp.text
501+
assert "flow-compact" in resp.text
502+
assert "flow-detailed-biz" in resp.text
503+
assert "flow-compact-biz" in resp.text
504+
505+
506+
@pytest.mark.anyio
507+
async def test_overview_has_business_graph():
508+
"""Overview should contain both technical and business project graphs."""
509+
scripts = [AnalyzedScript(path="a.py"), AnalyzedScript(path="b.py")]
510+
from visualpy.models import ScriptConnection
511+
connections = [ScriptConnection(source="a.py", target="b.py", type="import", detail="")]
512+
project = AnalyzedProject(path="/tmp", scripts=scripts, connections=connections)
513+
app = create_app(project)
514+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
515+
resp = await ac.get("/")
516+
# Both graph sources should be in the page
517+
assert "graph-tech" in resp.text or "graph LR" in resp.text
518+
assert resp.status_code == 200
519+
520+
521+
@pytest.mark.anyio
522+
async def test_translate_globals_registered():
523+
"""Jinja2 globals for translate functions should be accessible."""
524+
project = AnalyzedProject(path="/tmp", scripts=[AnalyzedScript(path="a.py")])
525+
app = create_app(project)
526+
# Check that the globals are registered in the template env
527+
from visualpy.translate import BUSINESS_LABELS
528+
# Access the templates through the app
529+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
530+
resp = await ac.get("/")
531+
# The overview page should render without errors (proves globals work)
532+
assert resp.status_code == 200
533+
534+
535+
@pytest.mark.anyio
536+
async def test_script_triggers_translated():
537+
"""Script view should contain translated trigger text."""
538+
from visualpy.models import Trigger
539+
script = AnalyzedScript(
540+
path="job.py",
541+
triggers=[Trigger(type="cli", detail="__main__ guard")],
542+
steps=[Step(line_number=1, type="output", description="print()")],
543+
)
544+
project = AnalyzedProject(path="/tmp", scripts=[script])
545+
app = create_app(project)
546+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
547+
resp = await ac.get("/script/job.py")
548+
assert "Can be run directly" in resp.text
549+
550+
551+
@pytest.mark.anyio
552+
async def test_script_secrets_translated():
553+
"""Script view should contain translated secret text."""
554+
script = AnalyzedScript(
555+
path="api.py",
556+
secrets=["AWS_SECRET_ACCESS_KEY"],
557+
steps=[Step(line_number=1, type="api_call", description="call()")],
558+
)
559+
project = AnalyzedProject(path="/tmp", scripts=[script])
560+
app = create_app(project)
561+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
562+
resp = await ac.get("/script/api.py")
563+
assert "AWS credentials" in resp.text

0 commit comments

Comments
 (0)