From 439ec5ca4ff4549f2f9534d772c81de4d96bded0 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 28 Jul 2026 23:47:46 +0500 Subject: [PATCH 1/2] fix: skip corrupted run state files in list_runs --- src/specify_cli/workflows/engine.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index 13fd633338..498123a782 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1583,8 +1583,11 @@ def list_runs(self) -> list[dict[str, Any]]: continue state_path = run_dir / "state.json" if state_path.exists(): - with open(state_path, encoding="utf-8") as f: - state_data = json.load(f) + try: + with open(state_path, encoding="utf-8") as f: + state_data = json.load(f) + except (json.JSONDecodeError, OSError): + continue runs.append(state_data) return runs From 98605360013cabc385fe3ed6c1488da91a389b80 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 29 Jul 2026 03:50:17 +0500 Subject: [PATCH 2/2] fix: address review comments - add UnicodeDecodeError, dict validation, and regression tests - Catch UnicodeDecodeError for invalid UTF-8 encoding - Validate loaded JSON is a dict with required 'run_id' key - Add 5 regression tests for corrupted state files Fixes #3814 --- src/specify_cli/workflows/engine.py | 4 +- tests/test_workflows.py | 88 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index 498123a782..442616ff48 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1586,7 +1586,9 @@ def list_runs(self) -> list[dict[str, Any]]: try: with open(state_path, encoding="utf-8") as f: state_data = json.load(f) - except (json.JSONDecodeError, OSError): + except (json.JSONDecodeError, OSError, UnicodeDecodeError): + continue + if not isinstance(state_data, dict) or "run_id" not in state_data: continue runs.append(state_data) return runs diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 9d02b3a6f1..915b0be3e0 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -6196,6 +6196,94 @@ def test_list_after_execution(self, project_dir): assert len(runs) == 1 assert runs[0]["workflow_id"] == "list-test" + def test_list_skips_malformed_json(self, project_dir): + from specify_cli.workflows.engine import WorkflowEngine + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-run" + bad_dir.mkdir(parents=True) + (bad_dir / "state.json").write_text("{invalid json", encoding="utf-8") + + engine = WorkflowEngine(project_dir) + assert engine.list_runs() == [] + + def test_list_skips_unreadable_file(self, project_dir): + import sys + import subprocess + from specify_cli.workflows.engine import WorkflowEngine + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-run" + bad_dir.mkdir(parents=True) + state_file = bad_dir / "state.json" + state_file.write_text('{"run_id": "x"}', encoding="utf-8") + + if sys.platform == "win32": + subprocess.run(["attrib", "+R", str(state_file)], check=True) + else: + state_file.chmod(0o000) + + try: + engine = WorkflowEngine(project_dir) + if sys.platform == "win32": + assert engine.list_runs() == [{"run_id": "x"}] + else: + assert engine.list_runs() == [] + finally: + if sys.platform == "win32": + subprocess.run(["attrib", "-R", str(state_file)], check=True) + else: + state_file.chmod(0o644) + + def test_list_skips_non_dict_payload(self, project_dir): + from specify_cli.workflows.engine import WorkflowEngine + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-run" + bad_dir.mkdir(parents=True) + (bad_dir / "state.json").write_text('["not", "a", "dict"]', encoding="utf-8") + + engine = WorkflowEngine(project_dir) + assert engine.list_runs() == [] + + def test_list_skips_empty_dict_payload(self, project_dir): + from specify_cli.workflows.engine import WorkflowEngine + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-run" + bad_dir.mkdir(parents=True) + (bad_dir / "state.json").write_text('{}', encoding="utf-8") + + engine = WorkflowEngine(project_dir) + assert engine.list_runs() == [] + + def test_list_skips_bad_file_with_valid_sibling(self, project_dir): + from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-run" + bad_dir.mkdir(parents=True) + (bad_dir / "state.json").write_text("{bad", encoding="utf-8") + + yaml_str = """ +schema_version: "1.0" +workflow: + id: "good-run" + name: "Good Run" + version: "1.0.0" +steps: + - id: step-one + type: shell + run: "echo test" +""" + definition = WorkflowDefinition.from_string(yaml_str) + engine = WorkflowEngine(project_dir) + engine.execute(definition) + + runs = engine.list_runs() + assert len(runs) == 1 + assert runs[0]["workflow_id"] == "good-run" + # ===== Workflow Registry Tests =====