Skip to content

Commit 1e85d4f

Browse files
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
1 parent 03d71b3 commit 1e85d4f

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

src/specify_cli/workflows/engine.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,8 +1710,13 @@ def list_runs(self) -> list[dict[str, Any]]:
17101710
continue
17111711
state_path = run_dir / "state.json"
17121712
if state_path.exists():
1713-
with open(state_path, encoding="utf-8") as f:
1714-
state_data = json.load(f)
1713+
try:
1714+
with open(state_path, encoding="utf-8") as f:
1715+
state_data = json.load(f)
1716+
except (json.JSONDecodeError, OSError, UnicodeDecodeError):
1717+
continue
1718+
if not isinstance(state_data, dict) or "run_id" not in state_data:
1719+
continue
17151720
runs.append(state_data)
17161721
return runs
17171722

tests/test_workflows.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7332,6 +7332,94 @@ def test_list_after_execution(self, project_dir):
73327332
assert len(runs) == 1
73337333
assert runs[0]["workflow_id"] == "list-test"
73347334

7335+
def test_list_skips_malformed_json(self, project_dir):
7336+
from specify_cli.workflows.engine import WorkflowEngine
7337+
7338+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7339+
bad_dir = runs_dir / "bad-run"
7340+
bad_dir.mkdir(parents=True)
7341+
(bad_dir / "state.json").write_text("{invalid json", encoding="utf-8")
7342+
7343+
engine = WorkflowEngine(project_dir)
7344+
assert engine.list_runs() == []
7345+
7346+
def test_list_skips_unreadable_file(self, project_dir):
7347+
import sys
7348+
import subprocess
7349+
from specify_cli.workflows.engine import WorkflowEngine
7350+
7351+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7352+
bad_dir = runs_dir / "bad-run"
7353+
bad_dir.mkdir(parents=True)
7354+
state_file = bad_dir / "state.json"
7355+
state_file.write_text('{"run_id": "x"}', encoding="utf-8")
7356+
7357+
if sys.platform == "win32":
7358+
subprocess.run(["attrib", "+R", str(state_file)], check=True)
7359+
else:
7360+
state_file.chmod(0o000)
7361+
7362+
try:
7363+
engine = WorkflowEngine(project_dir)
7364+
if sys.platform == "win32":
7365+
assert engine.list_runs() == [{"run_id": "x"}]
7366+
else:
7367+
assert engine.list_runs() == []
7368+
finally:
7369+
if sys.platform == "win32":
7370+
subprocess.run(["attrib", "-R", str(state_file)], check=True)
7371+
else:
7372+
state_file.chmod(0o644)
7373+
7374+
def test_list_skips_non_dict_payload(self, project_dir):
7375+
from specify_cli.workflows.engine import WorkflowEngine
7376+
7377+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7378+
bad_dir = runs_dir / "bad-run"
7379+
bad_dir.mkdir(parents=True)
7380+
(bad_dir / "state.json").write_text('["not", "a", "dict"]', encoding="utf-8")
7381+
7382+
engine = WorkflowEngine(project_dir)
7383+
assert engine.list_runs() == []
7384+
7385+
def test_list_skips_empty_dict_payload(self, project_dir):
7386+
from specify_cli.workflows.engine import WorkflowEngine
7387+
7388+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7389+
bad_dir = runs_dir / "bad-run"
7390+
bad_dir.mkdir(parents=True)
7391+
(bad_dir / "state.json").write_text('{}', encoding="utf-8")
7392+
7393+
engine = WorkflowEngine(project_dir)
7394+
assert engine.list_runs() == []
7395+
7396+
def test_list_skips_bad_file_with_valid_sibling(self, project_dir):
7397+
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
7398+
7399+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7400+
bad_dir = runs_dir / "bad-run"
7401+
bad_dir.mkdir(parents=True)
7402+
(bad_dir / "state.json").write_text("{bad", encoding="utf-8")
7403+
7404+
yaml_str = """
7405+
schema_version: "1.0"
7406+
workflow:
7407+
id: "good-run"
7408+
name: "Good Run"
7409+
version: "1.0.0"
7410+
steps:
7411+
- id: step-one
7412+
type: shell
7413+
run: "echo test"
7414+
"""
7415+
definition = WorkflowDefinition.from_string(yaml_str)
7416+
engine = WorkflowEngine(project_dir)
7417+
engine.execute(definition)
7418+
7419+
runs = engine.list_runs()
7420+
assert len(runs) == 1
7421+
assert runs[0]["workflow_id"] == "good-run"
7422+
73357423

73367424
# ===== Workflow Registry Tests =====
73377425

0 commit comments

Comments
 (0)