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
7 changes: 6 additions & 1 deletion src/specify_cli/workflows/overlays/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ def _parse_edit(edit_raw: dict[str, Any], idx: int) -> tuple[OverlayEdit | None,
else:
return None, f"Edit at index {idx} has no operation; expected one of {sorted(VALID_OPERATIONS)}."

if operation not in VALID_OPERATIONS:
# ``operation`` comes straight from hand-edited YAML, so it may be an
# unhashable mapping/sequence (``operation: {insert_after: a}`` when the
# shorthand form is nested by mistake). Membership-testing an unhashable
# value against the frozenset raises TypeError, which would escape this
# never-raising validator; check the type first, like 'anchor' below.
if not isinstance(operation, str) or operation not in VALID_OPERATIONS:
return None, f"Edit at index {idx} has invalid operation {operation!r}."

if not isinstance(anchor, str) or not anchor:
Expand Down
38 changes: 38 additions & 0 deletions tests/workflows/test_overlay_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,44 @@ def test_invalid_operation_field_rejected(self):
assert overlay is None
assert any("operation" in e.lower() for e in errors), errors

@pytest.mark.parametrize(
"operation",
[
{"insert_after": "a"},
["insert_after"],
],
)
def test_non_string_operation_rejected_without_raising(self, operation):
"""An unhashable 'operation' must be reported, not raised.

`VALID_OPERATIONS` is a frozenset, so `operation not in ...` hashes the
value. Nesting the shorthand form under the explicit key by mistake
(`operation: {insert_after: a}`) therefore raised
`TypeError: unhashable type: 'dict'` out of a validator whose docstring
promises "validation never raises" — and nothing upstream catches
TypeError, so the CLI died with a raw traceback.
"""
overlay, errors = validate_overlay_yaml(
{
"id": "ov",
"extends": "wf",
"priority": 10,
"edits": [
{
"operation": operation,
"anchor": "a",
"step": {
"id": "b",
"type": "command",
"command": "echo",
},
}
],
}
)
assert overlay is None
assert any("invalid operation" in err for err in errors), errors

def test_shorthand_and_explicit_mixed_list(self):
overlay, errors = validate_overlay_yaml(
{
Expand Down