From 4cbe273b5669a4980e3f3b7dfaeb3adf0ed85c5b Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:32:59 -0400 Subject: [PATCH] fix(rank): report a tracker that is not UTF-8 instead of a traceback (#639) * fix(rank): report a tracker that is not UTF-8 instead of a traceback A job_search_tracker.csv saved with a spreadsheet's plain "CSV" type is written in the ANSI code page, so one accented company name raised a UnicodeDecodeError traceback in `rank_state.py candidates` and stopped /rank at Step 1. Exit 1 with a one-line message instead. Co-Authored-By: Claude Opus 5.5 * changelog: place the #639 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Mads Lorentzen --- CHANGELOG.md | 6 ++++++ tests/test_rank_state.py | 13 +++++++++++++ tools/rank_state.py | 15 ++++++++++----- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b87adaed..102dac0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,12 @@ per-file diff commands. whole batch, although `apply` already collects per-result problems in `errors`. Each now becomes an `errors` row (exit 1) and the valid results in the same file are still written. +- **`rank_state.py candidates` reports a tracker that is not UTF-8 instead of crashing** (#639, `tools/rank_state.py`, + `tests/test_rank_state.py`) - a `job_search_tracker.csv` saved with a spreadsheet's plain "CSV" type is + written in the ANSI code page (cp1252 on most Western installs), so one accented company name such as + "Ørsted" raised a `UnicodeDecodeError` traceback and stopped `/rank` at Step 1. It now exits 1 with a + one-line message naming the file and asking for a CSV UTF-8 save, as `load_state` does for invalid JSON. + ## [1.7.3] - 2026-10-19 ### Added diff --git a/tests/test_rank_state.py b/tests/test_rank_state.py index fe15b238..a7233da3 100644 --- a/tests/test_rank_state.py +++ b/tests/test_rank_state.py @@ -160,6 +160,19 @@ class Candidates(RankStateCase): ) self.assertEqual(out["excluded_by_tracker"], int(excluded)) + def test_tracker_saved_in_a_legacy_code_page_is_reported_not_a_traceback(self): + # A spreadsheet's plain "CSV" save on Windows writes cp1252, not UTF-8. + self.write_state({"a": entry()}) + tracker = self.tmp / "tracker.csv" + tracker.write_bytes("date,company,role\r\n2026-08-01,Ørsted,SOC Analyst\r\n".encode("cp1252")) + proc = subprocess.run( + [sys.executable, str(TOOL), "candidates", "--state", str(self.state), "--tracker", str(tracker)], + capture_output=True, text=True, encoding="utf-8", + ) + self.assertEqual(proc.returncode, 1) + self.assertIn("is not UTF-8 text - re-save it as CSV UTF-8", proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + def test_focus_filters_on_title_company_and_stored_fit_notes(self): self.write_state( { diff --git a/tools/rank_state.py b/tools/rank_state.py index 89c47e83..93e5b898 100644 --- a/tools/rank_state.py +++ b/tools/rank_state.py @@ -149,11 +149,16 @@ def tracker_pairs(path: Path) -> set[tuple[str, str]]: import csv pairs = set() - with path.open(encoding="utf-8-sig", newline="") as fh: - for row in csv.DictReader(fh): - company, role = norm(row.get("company")), norm(row.get("role")) - if company: - pairs.add((company, role)) + try: + with path.open(encoding="utf-8-sig", newline="") as fh: + for row in csv.DictReader(fh): + company, role = norm(row.get("company")), norm(row.get("role")) + if company: + pairs.add((company, role)) + except UnicodeDecodeError: + # A spreadsheet's plain "CSV" save writes the ANSI code page (cp1252 on + # most Western installs), so any accented company name stops decoding. + sys.exit(f"{path} is not UTF-8 text - re-save it as CSV UTF-8") return pairs