From 9c802a4aa20b4825b9b7e2a24c1927e0ad018b88 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:14:09 -0400 Subject: [PATCH] fix(job-key): report a non-object state file in --audit instead of a traceback (#626) doc.get("seen", doc) raised AttributeError on a top-level array, and entry.get("url") raised it on an entry that was not an object, so the existing "expected an object of job entries" message was never reached. Guard both and exit 1 with a one-line message. Co-authored-by: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +++++ tests/test_job_key.py | 22 ++++++++++++++++++++++ tools/job_key.py | 6 +++++- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7424b14d..0f4b1b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,11 @@ per-file diff commands. ### Fixed +- **`job_key.py --audit` reports a state file that is not an object of entries** (`tools/job_key.py`, + `tests/test_job_key.py`) - a top-level JSON array, or an entry that is a string or number, crashed the + audit with an `AttributeError` traceback on `.get`, although the function already had a one-line error + for an unusable `seen` value. Both now print a one-line message naming the problem and exit 1. + - **`freehire-search` emits the contract's `YYYY-MM-DD` date instead of the API's ISO timestamp** (`.agents/skills/freehire-search/cli/src/helpers.ts`, `tests/parsing.test.ts`, `tests/commands.test.ts`, `SKILL.md`) - `toResult` passed `posted_at` through verbatim, so diff --git a/tests/test_job_key.py b/tests/test_job_key.py index 16f5a862..543bc70b 100644 --- a/tests/test_job_key.py +++ b/tests/test_job_key.py @@ -256,6 +256,28 @@ class AuditCLI(unittest.TestCase): self.assertEqual(code, 0) self.assertIn("acme_soc-analyst_athens", report["legacy_three_part_keys"]) + def run_audit_raw(self, document) -> subprocess.CompletedProcess: + import tempfile + + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh: + json.dump(document, fh) + path = fh.name + return subprocess.run( + [sys.executable, str(TOOL), "--audit", path], capture_output=True, text=True, encoding="utf-8" + ) + + def test_non_object_state_file_is_reported_not_a_traceback(self): + proc = self.run_audit_raw([]) + self.assertEqual(proc.returncode, 1) + self.assertIn("expected an object of job entries", proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + + def test_non_object_entry_is_reported_not_a_traceback(self): + proc = self.run_audit_raw({"seen": {"acme_soc-analyst": "oops"}}) + self.assertEqual(proc.returncode, 1) + self.assertIn("entry is not an object: acme_soc-analyst", proc.stderr) + self.assertNotIn("Traceback", proc.stderr) + if __name__ == "__main__": unittest.main() diff --git a/tools/job_key.py b/tools/job_key.py index 64964113..377c5fe9 100644 --- a/tools/job_key.py +++ b/tools/job_key.py @@ -183,10 +183,14 @@ def audit(path: Path) -> int: except (OSError, json.JSONDecodeError) as exc: print(f"cannot read {path}: {exc}", file=sys.stderr) return 1 - seen = doc.get("seen", doc) + seen = doc.get("seen", doc) if isinstance(doc, dict) else doc if not isinstance(seen, dict): print(f"{path}: expected an object of job entries", file=sys.stderr) return 1 + not_objects = [k for k, v in seen.items() if not isinstance(v, dict)] + if not_objects: + print(f"{path}: entry is not an object: {not_objects[0]}", file=sys.stderr) + return 1 malformed = [k for k in seen if not is_canonical(k) and not is_legacy_shape(k)] legacy = [k for k in seen if is_legacy_shape(k)]