feat(pipeline): visual module-card builder for Automated Workflows

Replaces the raw options_json data-editor table with a per-step "module
card" builder matching the locked design mockup
(layout-review/09_pipeline_runner.html): each step shows a friendly name +
caption, an enable toggle, ▲/▼/✕ reorder/remove controls, and a Configure
expander that renders that tool's own controls in plain language. Raw JSON
is demoted to an Advanced import/export section.

New src/gui/components/pipeline_modules.py holds the adapter-key→tool_id
friendly-name bridge, one plain-language config renderer per tool
(text_clean, format_standardize, missing, column_map, dedup — emitting the
exact JSON option shapes the core adapters accept), and render_step_card.
Steps live in session state as an ordered list with stable ids so widget
keys survive reorder/remove. Reorder is ▲/▼ buttons (no JS drag dependency).

The on-disk/CLI pipeline JSON format is unchanged — CLI and src/core
untouched. Adds tests/gui/test_pipeline_builder.py (AppTest) covering seed,
configure panels, toggle/add/remove, and a full run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 18:16:09 +00:00
parent fd9606c67b
commit 837f4b88b5
3 changed files with 645 additions and 126 deletions

View File

@@ -0,0 +1,91 @@
"""Pipeline Runner — visual module-card builder contract (AppTest).
Pins the behaviors the JSON-table → module-card rewrite introduced:
recommended steps seed as cards with friendly names, each step exposes a
plain-language Configure panel (no raw per-row JSON), steps can be toggled /
added / removed, JSON lives only under Advanced, and a run produces results
with friendly step names. The page's bare initial-render contract across junk
files is covered separately in ``tests/test_junk_corpus_tool_pages.py``.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from streamlit.testing.v1 import AppTest
_PAGE = (
Path(__file__).resolve().parent.parent.parent
/ "src" / "gui" / "pages" / "9_Pipeline_Runner.py"
)
_CSV = (
b"name,email,phone,signup_date\n"
b" Jane Doe ,jane@acme.io,512-555-0190,2024-01-04\n"
b"jane doe,JANE@ACME.IO,(512) 555-0190,01/04/2024\n"
b"Bob Smith,bob@globex.com,720.555.7781,2024-02-11\n"
)
def _app() -> AppTest:
at = AppTest.from_file(str(_PAGE), default_timeout=30)
at.session_state["home_uploaded_bytes"] = _CSV
at.session_state["home_uploaded_name"] = "customers.csv"
at.session_state["home_uploaded_size"] = len(_CSV)
return at.run()
def test_recommended_steps_seed_as_named_cards():
at = _app()
assert not at.exception
tools = [s["tool"] for s in at.session_state["pipeline_steps"]]
assert tools == ["text_clean", "format_standardize", "missing", "dedup"]
md = " ".join(m.value for m in at.markdown)
for friendly in ("Clean Text", "Standardize Formats",
"Fix Missing Values", "Find Duplicates"):
assert friendly in md
def test_each_step_has_a_configure_panel_and_json_is_advanced_only():
at = _app()
labels = [e.label for e in at.get("expander")]
assert any(l.startswith("Configure: Clean Text") for l in labels)
assert any(l.startswith("Configure: Find Duplicates") for l in labels)
# Raw JSON is import/export only — never a per-step editing surface.
assert any("Advanced — import / export" in l for l in labels)
def test_toggle_disables_step_and_persists():
at = _app()
at.toggle[0].set_value(False).run()
assert at.session_state["pipeline_steps"][0]["enabled"] is False
def test_add_step_appends_a_working_config_panel():
at = _app()
[s for s in at.selectbox if s.key == "pipeline_add_tool"][0].set_value("column_map").run()
[b for b in at.button if "Add step" in b.label][0].click().run()
assert not at.exception
assert at.session_state["pipeline_steps"][-1]["tool"] == "column_map"
labels = [e.label for e in at.get("expander")]
assert any(l.startswith("Configure: Map Columns") for l in labels)
def test_remove_step_drops_it():
at = _app()
before = len(at.session_state["pipeline_steps"])
# The first ✕ remove button in the card stack.
[b for b in at.button if b.label == ""][0].click().run()
assert not at.exception
assert len(at.session_state["pipeline_steps"]) == before - 1
def test_run_produces_results_with_friendly_names():
at = _app()
[b for b in at.button if b.label == "Run Pipeline"][0].click().run()
assert not at.exception, at.exception
assert "pipeline_result" in at.session_state
res = at.session_state["pipeline_result"]
assert res.initial_rows == 3 and res.final_rows == 2 # the two Jane rows merge
assert all(sr.error is None for sr in res.step_results)