""" tests/test_regression.py ------------------------ Regression tests for the compose pipeline. For each downloaded template in downloads/, run compose_template() and compare the output against the snapshot in tests/fixtures/expected/. These tests require no live API calls. They verify that changes to the compose pipeline don't silently break existing template conversions. To update snapshots after an intentional change: pytest tests/test_regression.py --update-snapshots """ import json import os import sys import tempfile import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from compose_docusign_template import compose_template DOWNLOADS_DIR = os.path.join(os.path.dirname(__file__), "..", "downloads") FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures", "expected") # Templates with real downloaded data to test against REGRESSION_TEMPLATES = [ "David Tag Demo Form__CBJCHBCA", "_DEMO USE ONLY_ NDA__CBJCHBCA", "Rob Test__CBJCHBCA", ] @pytest.fixture def update_snapshots(request): return request.config.getoption("--update-snapshots", default=False) @pytest.mark.parametrize("template_name", REGRESSION_TEMPLATES) def test_compose_regression(template_name, update_snapshots): """ Compose output for each template must match the stored snapshot. Run with --update-snapshots to regenerate. """ template_dir = os.path.join(DOWNLOADS_DIR, template_name) if not os.path.isdir(template_dir): pytest.skip(f"Downloaded template not found: {template_name}") snapshot_path = os.path.join(FIXTURES_DIR, f"{template_name}.json") with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as tf: output_path = tf.name try: result, warnings, _ = compose_template(template_dir, output_path) if update_snapshots: os.makedirs(FIXTURES_DIR, exist_ok=True) with open(snapshot_path, "w") as f: json.dump(result, f, indent=2) pytest.skip(f"Snapshot updated for {template_name}") if not os.path.exists(snapshot_path): pytest.fail( f"No snapshot for '{template_name}'. " f"Run with --update-snapshots to create it." ) with open(snapshot_path) as f: expected = json.load(f) # Compare key structural properties assert result.get("name") == expected.get("name"), \ f"Template name mismatch for {template_name}" # Recipients result_roles = sorted([r.get("roleName", "") for r in result.get("recipients", {}).get("signers", [])]) expected_roles = sorted([r.get("roleName", "") for r in expected.get("recipients", {}).get("signers", [])]) assert result_roles == expected_roles, \ f"Recipient roles changed for {template_name}: {result_roles} != {expected_roles}" # Tab counts per type — must not regress result_tabs = _count_tabs(result) expected_tabs = _count_tabs(expected) for tab_type, count in expected_tabs.items(): actual = result_tabs.get(tab_type, 0) assert actual == count, ( f"Tab count regression in {template_name}: " f"{tab_type} expected {count}, got {actual}" ) finally: if os.path.exists(output_path): os.unlink(output_path) def _count_tabs(template: dict) -> dict: """Count total tabs of each type across all signers.""" counts = {} for signer in template.get("recipients", {}).get("signers", []): tabs = signer.get("tabs", {}) for tab_type, items in tabs.items(): if isinstance(items, list): counts[tab_type] = counts.get(tab_type, 0) + len(items) return counts def test_no_tabs_lost_on_recompose(): """ Sanity check: every downloaded template must produce at least one tab. Catches complete compose failures silently returning empty output. """ for template_name in REGRESSION_TEMPLATES: template_dir = os.path.join(DOWNLOADS_DIR, template_name) if not os.path.isdir(template_dir): continue with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tf: output_path = tf.name try: result, _, _issues = compose_template(template_dir, output_path) total_tabs = sum(_count_tabs(result).values()) assert total_tabs > 0, f"No tabs produced for {template_name}" finally: if os.path.exists(output_path): os.unlink(output_path)