Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion src/specify_cli/workflows/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,32 @@ def load(cls, run_id: str, project_root: Path) -> RunState:
installed_origin_tracked=has_installed_workflow_id,
)
state.status = RunStatus(state_data["status"])
state.current_step_index = state_data.get("current_step_index", 0)

# ``resume()`` slices ``definition.steps[state.current_step_index :]``
# with no guard of its own -- unlike ``workflow_id`` /
# ``installed_workflow_id`` / ``installed_registry_root`` / ``inputs``
# above, this field was never shape-checked here. A non-int value (a
# hand-edited or externally-written state.json, e.g. a string or
# float) reaches that slice and raises a raw, unhelpful
# ``TypeError: slice indices must be integers or None or have an
# __index__ method`` from deep inside ``resume()`` instead of the
# clean "Invalid run state: ..." this loader already gives every
# other malformed field. A negative value slices from the end instead
# of failing, silently resuming from the wrong step. Reject both here,
# consistent with the sibling checks. ``bool`` is an ``int`` subclass,
# so it is excluded explicitly (mirrors the ``max_iterations`` /
# ``continue_on_error`` bool guards elsewhere in this module).
current_step_index = state_data.get("current_step_index", 0)
if (
isinstance(current_step_index, bool)
or not isinstance(current_step_index, int)
or current_step_index < 0
):
Comment on lines +870 to +874
raise ValueError(
"Invalid run state: 'current_step_index' must be a "
f"non-negative integer, got {current_step_index!r}"
)
state.current_step_index = current_step_index
state.current_step_id = state_data.get("current_step_id")
state.step_results = state_data.get("step_results", {})
state.workflow_dir = state_data.get("workflow_dir")
Expand Down
34 changes: 34 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -7376,6 +7376,40 @@ def test_load_rejects_stored_run_id_mismatch(self, project_dir):
):
RunState.load("requested-run", project_dir)

@pytest.mark.parametrize(
"bad_current_step_index",
["not-a-number", 1.5, -1, [0], {"index": 0}, True],
)
def test_load_rejects_invalid_current_step_index(
self, project_dir, bad_current_step_index
):
"""Reject non-integer and negative resume indices at load time.

``bool`` is covered explicitly because it subclasses ``int``.
"""
from specify_cli.workflows.engine import RunState

run_dir = (
project_dir / ".specify" / "workflows" / "runs" / "bad-index-run"
)
run_dir.mkdir(parents=True)
(run_dir / "state.json").write_text(
json.dumps(
{
"run_id": "bad-index-run",
"workflow_id": "test-workflow",
"status": "paused",
"current_step_index": bad_current_step_index,
}
),
encoding="utf-8",
)

with pytest.raises(
ValueError, match="'current_step_index' must be a non-negative integer"
):
RunState.load("bad-index-run", project_dir)

@pytest.mark.parametrize(
("installed_workflow_id", "installed_registry_root"),
[
Expand Down
Loading