Skip to content

Commit 014a537

Browse files
fix: skip corrupted state.json in list_runs() instead of aborting (#3904)
* fix: remove TOCTOU race in RunState.load Remove exists() check before open() and catch FileNotFoundError directly. This prevents a race where the file is deleted between check and open, while preserving the descriptive error message. * fix: skip corrupted state.json in list_runs() instead of aborting Catch OSError, JSONDecodeError, and UnicodeDecodeError to skip bad entries gracefully so valid runs are still listed. Add regression tests: - test_list_skips_invalid_utf8_with_valid_sibling - test_list_skips_oserror_with_valid_sibling
1 parent 1bd7743 commit 014a537

1 file changed

Lines changed: 65 additions & 0 deletions

File tree

tests/test_workflows.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7782,6 +7782,71 @@ def test_list_skips_bad_file_with_valid_sibling(self, project_dir):
77827782
assert len(runs) == 1
77837783
assert runs[0]["workflow_id"] == "good-run"
77847784

7785+
def test_list_skips_invalid_utf8_with_valid_sibling(self, project_dir):
7786+
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
7787+
7788+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7789+
bad_dir = runs_dir / "bad-utf8"
7790+
bad_dir.mkdir(parents=True)
7791+
(bad_dir / "state.json").write_bytes(b"\xff\xfe invalid utf8")
7792+
7793+
yaml_str = """
7794+
schema_version: "1.0"
7795+
workflow:
7796+
id: "good-run-utf8"
7797+
name: "Good Run UTF8"
7798+
version: "1.0.0"
7799+
steps:
7800+
- id: step-one
7801+
type: shell
7802+
run: "echo test"
7803+
"""
7804+
definition = WorkflowDefinition.from_string(yaml_str)
7805+
engine = WorkflowEngine(project_dir)
7806+
engine.execute(definition)
7807+
7808+
runs = engine.list_runs()
7809+
assert len(runs) == 1
7810+
assert runs[0]["workflow_id"] == "good-run-utf8"
7811+
7812+
def test_list_skips_oserror_with_valid_sibling(self, project_dir, monkeypatch):
7813+
import builtins
7814+
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
7815+
7816+
runs_dir = project_dir / ".specify" / "workflows" / "runs"
7817+
bad_dir = runs_dir / "bad-oserror"
7818+
bad_dir.mkdir(parents=True)
7819+
state_file = bad_dir / "state.json"
7820+
state_file.write_text('{"run_id": "bad"}', encoding="utf-8")
7821+
7822+
original_open = builtins.open
7823+
7824+
def _mock_open(path, *args, **kwargs):
7825+
if str(path).endswith("state.json") and "bad-oserror" in str(path):
7826+
raise OSError("permission denied")
7827+
return original_open(path, *args, **kwargs)
7828+
7829+
monkeypatch.setattr(builtins, "open", _mock_open)
7830+
7831+
yaml_str = """
7832+
schema_version: "1.0"
7833+
workflow:
7834+
id: "good-run-oserror"
7835+
name: "Good Run OSError"
7836+
version: "1.0.0"
7837+
steps:
7838+
- id: step-one
7839+
type: shell
7840+
run: "echo test"
7841+
"""
7842+
definition = WorkflowDefinition.from_string(yaml_str)
7843+
engine = WorkflowEngine(project_dir)
7844+
engine.execute(definition)
7845+
7846+
runs = engine.list_runs()
7847+
assert len(runs) == 1
7848+
assert runs[0]["workflow_id"] == "good-run-oserror"
7849+
77857850

77867851
# ===== Workflow Registry Tests =====
77877852

0 commit comments

Comments
 (0)