from __future__ import annotations from typing import Any, get_args from pydantic import ValidationError import pytest from vibe.core.config import ( AddOperationPatch, RemoveOperationPatch, ReplaceOperationPatch, ) from vibe.core.config.patch import ConfigPatch, PatchOp @pytest.mark.parametrize( ("operation", "expected"), [ ( AddOperationPatch(path="/tools/disabled_tools/0", value="bash"), {"op": "add", "path": "/tools/disabled_tools/0", "value": "bash"}, ), ( ReplaceOperationPatch(path="/active_model", value="devstral-small"), {"op": "replace", "path": "/active_model", "value": "devstral-small"}, ), ( RemoveOperationPatch(path="/tools/deprecated_setting"), {"op": "remove", "path": "/tools/deprecated_setting"}, ), ], ) def test_json_patch_operations_convert_to_json_patch_payload( operation: AddOperationPatch | ReplaceOperationPatch | RemoveOperationPatch, expected: dict[str, Any], ) -> None: assert operation.to_json_patch() == expected def test_patch_op_union_contains_all_operations() -> None: assert set(get_args(PatchOp.__value__)) == { AddOperationPatch, ReplaceOperationPatch, RemoveOperationPatch, } @pytest.mark.parametrize( "factory", [ lambda path: AddOperationPatch(path=path, value="value"), lambda path: ReplaceOperationPatch(path=path, value="value"), lambda path: RemoveOperationPatch(path=path), ], ) def test_json_patch_operations_reject_non_pointer_paths(factory: Any) -> None: with pytest.raises(ValidationError, match="valid JSON Pointer"): factory("tools.disabled_tools") @pytest.mark.parametrize( "factory", [ lambda path: AddOperationPatch(path=path, value="value"), lambda path: ReplaceOperationPatch(path=path, value="value"), lambda path: RemoveOperationPatch(path=path), ], ) def test_json_patch_operations_reject_invalid_escapes(factory: Any) -> None: with pytest.raises(ValidationError, match="valid JSON Pointer"): factory("/tools/~2") def test_json_patch_operations_accept_slash_prefixed_paths() -> None: op = ReplaceOperationPatch(path="/", value={"active_model": "devstral-small"}) assert op.path == "/" def test_config_patch_stores_operations_and_metadata() -> None: op = ReplaceOperationPatch(path="/active_model", value="devstral-small") patch = ConfigPatch(op, fingerprint="fp-1", reason="test") assert patch.operations == [op] assert patch.fingerprint == "fp-1" assert patch.reason == "test" def test_config_patch_defaults() -> None: patch = ConfigPatch(fingerprint="fp-1") assert patch.reason == "" assert patch.operations == [] def test_config_patch_accepts_multiple_operations() -> None: ops = [ ReplaceOperationPatch(path="/active_model", value="devstral-small"), AddOperationPatch(path="/tools/disabled_tools/-", value="bash"), ] patch = ConfigPatch(*ops, fingerprint="fp-1") assert patch.operations == ops def test_config_patch_add_appends_operations() -> None: patch = ConfigPatch( ReplaceOperationPatch(path="/active_model", value="devstral-small"), fingerprint="fp-1", ) patch.add(RemoveOperationPatch(path="/tools/deprecated_setting")) assert patch.operations == [ ReplaceOperationPatch(path="/active_model", value="devstral-small"), RemoveOperationPatch(path="/tools/deprecated_setting"), ] def test_config_patch_add_returns_self() -> None: patch = ConfigPatch(fingerprint="fp-1") result = patch.add( ReplaceOperationPatch(path="/active_model", value="devstral-small") ) assert result is patch def test_config_patch_add_accepts_multiple_operations() -> None: patch = ConfigPatch(fingerprint="fp-1") patch.add( ReplaceOperationPatch(path="/active_model", value="devstral-small"), AddOperationPatch(path="/tools/disabled_tools/-", value="bash"), ) assert len(patch.operations) == 2 def test_config_patch_add_is_chainable() -> None: patch = ( ConfigPatch(fingerprint="fp-1") .add(ReplaceOperationPatch(path="/active_model", value="devstral-small")) .add(RemoveOperationPatch(path="/tools/deprecated_setting")) ) assert len(patch.operations) == 2 def test_config_patch_to_json_patch_from_wrappers() -> None: patch = ConfigPatch( ReplaceOperationPatch(path="/active_model", value="devstral-small"), AddOperationPatch(path="/tools/disabled_tools/-", value="bash"), RemoveOperationPatch(path="/tools/deprecated_setting"), fingerprint="fp-1", ) assert patch.to_json_patch() == [ {"op": "replace", "path": "/active_model", "value": "devstral-small"}, {"op": "add", "path": "/tools/disabled_tools/-", "value": "bash"}, {"op": "remove", "path": "/tools/deprecated_setting"}, ] def test_config_patch_describe_add_operation() -> None: patch = ConfigPatch( AddOperationPatch(path="/tools/disabled_tools/-", value="bash"), fingerprint="fp-1", ) assert patch.describe() == ["add '/tools/disabled_tools/-' = 'bash'"] def test_config_patch_describe_replace_operation() -> None: patch = ConfigPatch( ReplaceOperationPatch(path="/active_model", value="devstral-small"), fingerprint="fp-1", ) assert patch.describe() == ["replace '/active_model' = 'devstral-small'"] def test_config_patch_describe_remove_operation() -> None: patch = ConfigPatch( RemoveOperationPatch(path="/tools/deprecated_setting"), fingerprint="fp-1" ) assert patch.describe() == ["remove '/tools/deprecated_setting'"] def test_config_patch_describe_empty_returns_empty_list() -> None: patch = ConfigPatch(fingerprint="fp-1") assert patch.describe() == [] def test_config_patch_describe_multiple_operations() -> None: patch = ConfigPatch( ReplaceOperationPatch(path="/active_model", value="devstral-small"), AddOperationPatch(path="/tools/disabled_tools/-", value="bash"), RemoveOperationPatch(path="/tools/deprecated_setting"), fingerprint="fp-1", ) assert patch.describe() == [ "replace '/active_model' = 'devstral-small'", "add '/tools/disabled_tools/-' = 'bash'", "remove '/tools/deprecated_setting'", ] def test_scenario_build_patch_incrementally() -> None: patch = ConfigPatch(fingerprint="fp-abc", reason="/model command") patch.add(ReplaceOperationPatch(path="/active_model", value="devstral-small")) patch.add(AddOperationPatch(path="/tools/disabled_tools/-", value="bash")) assert patch.fingerprint == "fp-abc" assert patch.reason == "/model command" assert len(patch.operations) == 2 assert patch.describe() == [ "replace '/active_model' = 'devstral-small'", "add '/tools/disabled_tools/-' = 'bash'", ]