diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index 835183a2cb..b9bf837017 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -743,12 +743,13 @@ def load(cls, run_id: str, project_root: Path) -> RunState: cls._validate_run_id(run_id) runs_dir = project_root / ".specify" / "workflows" / "runs" / run_id state_path = runs_dir / "state.json" - if not state_path.exists(): + + try: + with open(state_path, encoding="utf-8") as f: + state_data = json.load(f) + except FileNotFoundError: msg = f"Run state not found: {state_path}" raise FileNotFoundError(msg) - - with open(state_path, encoding="utf-8") as f: - state_data = json.load(f) if not isinstance(state_data, dict): raise ValueError("Invalid run state: expected a JSON object") missing_fields = [ diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 95baf22d3c..50d69b98a5 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -7453,6 +7453,71 @@ def test_list_skips_bad_file_with_valid_sibling(self, project_dir): assert len(runs) == 1 assert runs[0]["workflow_id"] == "good-run" + def test_list_skips_invalid_utf8_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-utf8" + bad_dir.mkdir(parents=True) + (bad_dir / "state.json").write_bytes(b"\xff\xfe invalid utf8") + + yaml_str = """ +schema_version: "1.0" +workflow: + id: "good-run-utf8" + name: "Good Run UTF8" + 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-utf8" + + def test_list_skips_oserror_with_valid_sibling(self, project_dir, monkeypatch): + import builtins + from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition + + runs_dir = project_dir / ".specify" / "workflows" / "runs" + bad_dir = runs_dir / "bad-oserror" + bad_dir.mkdir(parents=True) + state_file = bad_dir / "state.json" + state_file.write_text('{"run_id": "bad"}', encoding="utf-8") + + original_open = builtins.open + + def _mock_open(path, *args, **kwargs): + if str(path).endswith("state.json") and "bad-oserror" in str(path): + raise OSError("permission denied") + return original_open(path, *args, **kwargs) + + monkeypatch.setattr(builtins, "open", _mock_open) + + yaml_str = """ +schema_version: "1.0" +workflow: + id: "good-run-oserror" + name: "Good Run OSError" + 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-oserror" + # ===== Workflow Registry Tests =====