From 362ef6a522b30ce72d8c87bf492b8c7a21cb1b8e Mon Sep 17 00:00:00 2001 From: Ayobami Adegoke Date: Mon, 21 Sep 2026 07:20:50 +0100 Subject: [PATCH] test(salary): exercise real workbook conversion through the CLI (#489) --- .github/workflows/ci.yml | 2 + CHANGELOG.md | 9 ++ .../test_convert_salary_excel_integration.py | 90 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 tests/test_convert_salary_excel_integration.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbd03c57..89f8d059 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,8 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} + - name: Install Excel integration test dependency + run: python -m pip install openpyxl==3.1.5 - run: python -m unittest discover -s tests -t . -v dependency-review: diff --git a/CHANGELOG.md b/CHANGELOG.md index f01f9423..32639980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ per-file diff commands. ### Added +- **Real Excel workbook integration tests for the salary converter** + (`tests/test_convert_salary_excel_integration.py`, `.github/workflows/ci.yml`) - + generate temporary `.xlsx` files and invoke the documented converter CLI, + checking multiple worksheets, metadata options, Unicode text, localized + numbers, and compatibility with salary lookup. A workbook without salary + headers must fail without creating an output file. CI installs `openpyxl` + across the Python matrix; local runs without this optional dependency skip + the two integration cases while retaining the existing dependency-free tests. + - **`documents/projects/` portfolio ingestion in `/setup` (Path A)** (`documents/README.md`, `.claude/commands/setup.md`, `.claude/commands/reset.md`, `tests/test_setup_command.py`) - onboards project writeups, case studies, and documentation (`.md`, `.txt`, `.pdf`) diff --git a/tests/test_convert_salary_excel_integration.py b/tests/test_convert_salary_excel_integration.py new file mode 100644 index 00000000..d1e3e751 --- /dev/null +++ b/tests/test_convert_salary_excel_integration.py @@ -0,0 +1,90 @@ +"""Exercise the documented Excel -> converter CLI -> salary JSON path.""" +import json +import subprocess +import sys +import unittest +from pathlib import Path +from tempfile import TemporaryDirectory + +try: + import openpyxl +except ImportError: + openpyxl = None + +from salary_lookup import collect_validation_issues, format_entry + +TOOL = Path(__file__).resolve().parent.parent / "tools" / "convert_salary_excel.py" + + +@unittest.skipUnless(openpyxl, "requires optional openpyxl (installed in CI)") +class ConverterCLI(unittest.TestCase): + def setUp(self): + directory = TemporaryDirectory() + self.addCleanup(directory.cleanup) + self.root = Path(directory.name) + self.workbook = self.root / "salary input.xlsx" + self.output = self.root / "salary output.json" + + def run_converter(self, *args): + return subprocess.run( + [sys.executable, str(TOOL), str(self.workbook), + "--output", str(self.output), *args], + cwd=self.root, capture_output=True, text=True, + ) + + def test_real_workbook_round_trips_multiple_sheets_and_metadata(self): + book = openpyxl.Workbook() + try: + first = book.active + first.title = "All employees" + first.append(["Company", "City", "Count", "Index"]) + first.append(["Acme Corp", "Copenhagen", 500, 108.5]) + first.append(["Café Étoile", "Paris", 12, "102,5"]) + second = book.create_sheet("Engineering") + second.append(["Company", "City", "Engineering Count", "Engineering Index"]) + second.append(["Other Corp", "Berlin", 8, 112.5]) + book.save(self.workbook) + finally: + book.close() + + result = self.run_converter( + "--source", "Integration fixture", "--baseline", "100", + "--baseline-desc", "Index 100 = median salary", + ) + self.assertEqual(result.returncode, 0, result.stderr) + data = json.loads(self.output.read_text(encoding="utf-8")) + self.assertEqual(data["metadata"], { + "source": "Integration fixture", "index_baseline": 100, + "index_label": "Index", "baseline_description": "Index 100 = median salary", + }) + self.assertEqual(data["companies"], [ + {"company": "Acme Corp", "city": "Copenhagen", "categories": { + "all_employees": {"count": 500, "index": 108.5}}}, + {"company": "Café Étoile", "city": "Paris", "categories": { + "all_employees": {"count": 12, "index": 102.5}}}, + {"company": "Other Corp", "city": "Berlin", "categories": { + "engineering": {"count": 8, "index": 112.5}}}, + ]) + self.assertEqual(collect_validation_issues(data), ([], [])) + rendered = format_entry(data["companies"][0], data["metadata"]) + self.assertIn("All Employees", rendered) + self.assertIn("+8.5%", rendered) + self.assertNotIn("N/A*", rendered) + + def test_workbook_without_salary_headers_fails_without_writing_output(self): + book = openpyxl.Workbook() + try: + book.active.append(["Notes", "Comments"]) + book.active.append(["No salary data", "Just a worksheet"]) + book.save(self.workbook) + finally: + book.close() + + result = self.run_converter() + self.assertEqual(result.returncode, 1, result.stderr) + self.assertIn("No data could be parsed", result.stderr) + self.assertFalse(self.output.exists()) + + +if __name__ == "__main__": + unittest.main()