From 1e85d4ff53d1d51adbb06b27fe602fd2a37d54dc Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 5 Aug 2026 18:11:38 +0500 Subject: [PATCH] fix: skip corrupted run state files in list_runs (#3814) * fix: skip corrupted run state files in list_runs * 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 | 9 ++- tests/test_workflows.py | 88 +++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index a478aafdd..835183a2c 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1710,8 +1710,13 @@ class WorkflowEngine: 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, 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 b9cfd67c0..8ca7dca50 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -7332,6 +7332,94 @@ steps: 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 =====