diff --git a/scripts/odysseus-cookbook b/scripts/odysseus-cookbook index 845a2db..860a790 100755 --- a/scripts/odysseus-cookbook +++ b/scripts/odysseus-cookbook @@ -411,6 +411,8 @@ def cmd_state_set(args) -> None: obj = json.loads(data) except json.JSONDecodeError as e: fail(f"invalid JSON on stdin: {e}") + if not isinstance(obj, dict): + fail("invalid cookbook state: expected a JSON object") _STATE_PATH.parent.mkdir(parents=True, exist_ok=True) # Backup the existing state — undo button if a bad pipe clobbers it. if _STATE_PATH.exists(): diff --git a/tests/test_cookbook_cli_state.py b/tests/test_cookbook_cli_state.py new file mode 100644 index 0000000..5673d5d --- /dev/null +++ b/tests/test_cookbook_cli_state.py @@ -0,0 +1,30 @@ +import importlib.machinery +import importlib.util +import io +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] + + +def _load_cli(): + path = ROOT / "scripts" / "odysseus-cookbook" + loader = importlib.machinery.SourceFileLoader("odysseus_cookbook_cli", str(path)) + spec = importlib.util.spec_from_loader(loader.name, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def test_state_set_rejects_non_object_json(tmp_path, monkeypatch, capsys): + cli = _load_cli() + cli._STATE_PATH = tmp_path / "cookbook_state.json" + monkeypatch.setattr(cli.sys, "stdin", io.StringIO("[]")) + + with pytest.raises(SystemExit): + cli.cmd_state_set(type("Args", (), {})()) + + assert "expected a JSON object" in capsys.readouterr().err + assert not cli._STATE_PATH.exists()