""" Tests for core.state.StateStore Covers: - get / set / delete basic operations - default value on missing key - watcher callbacks on change - watcher not called when value unchanged - watcher wildcard patterns - unwatch removes callback - update() for bulk set - snapshot() returns deep copy - bus integration (state publishes to bus) - keys() and __contains__ """ from __future__ import annotations from typing import Any, List from unittest.mock import AsyncMock, MagicMock, patch import pytest from core.state import StateStore # ── Fixtures ────────────────────────────────────────────────────────────── @pytest.fixture def store() -> StateStore: return StateStore() @pytest.fixture def store_with_bus(): bus = MagicMock() bus.publish_sync = MagicMock() s = StateStore(bus=bus) return s, bus # ── Basic get / set ────────────────────────────────────────────────────── def test_set_and_get(store: StateStore) -> None: store.set("key.a", 42) assert store.get("key.a") == 42 def test_get_missing_key_returns_default(store: StateStore) -> None: assert store.get("does.not.exist") is None assert store.get("does.not.exist", "fallback") == "fallback" def test_set_overwrites_value(store: StateStore) -> None: store.set("x", 1) store.set("x", 2) assert store.get("x") == 2 def test_get_returns_deep_copy(store: StateStore) -> None: store.set("obj", {"a": [1, 2, 3]}) got = store.get("obj") got["a"].append(99) assert store.get("obj") == {"a": [1, 2, 3]} # original unchanged # ── Delete ──────────────────────────────────────────────────────────────── def test_delete_existing_key(store: StateStore) -> None: store.set("del.me", "value") assert store.delete("del.me") is True assert store.get("del.me") is None def test_delete_missing_key_returns_false(store: StateStore) -> None: assert store.delete("ghost.key") is False # ── Update ──────────────────────────────────────────────────────────────── def test_update_sets_multiple_keys(store: StateStore) -> None: store.update({"a": 1, "b": 2, "c": 3}) assert store.get("a") == 1 assert store.get("b") == 2 assert store.get("c") == 3 # ── Watchers ───────────────────────────────────────────────────────────── def test_watcher_called_on_change(store: StateStore) -> None: events: List[tuple] = [] def cb(key: str, old: Any, new: Any) -> None: events.append((key, old, new)) store.watch("watched.key", cb) store.set("watched.key", "hello") assert len(events) == 1 assert events[0] == ("watched.key", None, "hello") def test_watcher_called_with_old_and_new(store: StateStore) -> None: events: List[tuple] = [] store.set("my.key", "initial") def cb(key: str, old: Any, new: Any) -> None: events.append((old, new)) store.watch("my.key", cb) store.set("my.key", "updated") assert events == [("initial", "updated")] def test_watcher_not_called_when_value_unchanged(store: StateStore) -> None: events: List[tuple] = [] def cb(key, old, new): events.append((old, new)) store.set("stable", 100) store.watch("stable", cb) store.set("stable", 100) # same value assert len(events) == 0 def test_multiple_watchers_on_same_key(store: StateStore) -> None: results_a: list = [] results_b: list = [] store.watch("key", lambda k, o, n: results_a.append(n)) store.watch("key", lambda k, o, n: results_b.append(n)) store.set("key", "fire") assert results_a == ["fire"] assert results_b == ["fire"] def test_watcher_on_delete(store: StateStore) -> None: events: list = [] store.set("dying.key", "alive") store.watch("dying.key", lambda k, o, n: events.append((o, n))) store.delete("dying.key") assert events == [("alive", None)] # ── Wildcard watchers ───────────────────────────────────────────────────── def test_wildcard_watcher_fires_for_matching_keys(store: StateStore) -> None: events: list = [] store.watch("network.*", lambda k, o, n: events.append(k)) store.set("network.interfaces", {}) store.set("network.stats", {}) store.set("system.cpu", {}) # should NOT trigger assert "network.interfaces" in events assert "network.stats" in events assert "system.cpu" not in events def test_exact_and_wildcard_both_fired(store: StateStore) -> None: exact: list = [] wild: list = [] store.watch("net.iface", lambda k, o, n: exact.append(n)) store.watch("net.*", lambda k, o, n: wild.append(n)) store.set("net.iface", "up") assert exact == ["up"] assert wild == ["up"] # ── Unwatch ─────────────────────────────────────────────────────────────── def test_unwatch_stops_callback(store: StateStore) -> None: events: list = [] def cb(k, o, n): events.append(n) wid = store.watch("remove.me", cb) store.set("remove.me", 1) assert len(events) == 1 store.unwatch(wid) store.set("remove.me", 2) assert len(events) == 1 # no new call def test_unwatch_unknown_id_returns_false(store: StateStore) -> None: assert store.unwatch("not-a-real-id") is False # ── Snapshot ────────────────────────────────────────────────────────────── def test_snapshot_returns_full_state(store: StateStore) -> None: store.set("a", 1) store.set("b", {"nested": True}) snap = store.snapshot() assert snap["a"] == 1 assert snap["b"] == {"nested": True} def test_snapshot_is_deep_copy(store: StateStore) -> None: store.set("obj", [1, 2, 3]) snap = store.snapshot() snap["obj"].append(99) assert store.get("obj") == [1, 2, 3] # ── Keys and contains ───────────────────────────────────────────────────── def test_keys_returns_all_keys(store: StateStore) -> None: store.set("x", 1) store.set("y", 2) assert set(store.keys()) == {"x", "y"} def test_contains_operator(store: StateStore) -> None: store.set("present", True) assert "present" in store assert "absent" not in store # ── Bus integration ─────────────────────────────────────────────────────── def test_bus_publish_called_on_set(store_with_bus) -> None: store, bus = store_with_bus store.set("some.key", "value") bus.publish_sync.assert_called_once_with( "state.some.key", {"key": "some.key", "value": "value"} ) def test_bus_not_called_when_value_unchanged(store_with_bus) -> None: store, bus = store_with_bus store.set("stable", 42) bus.publish_sync.reset_mock() store.set("stable", 42) # unchanged bus.publish_sync.assert_not_called() def test_bus_called_on_delete(store_with_bus) -> None: store, bus = store_with_bus store.set("temp", "here") bus.publish_sync.reset_mock() store.delete("temp") bus.publish_sync.assert_called_once_with( "state.temp", {"key": "temp", "value": None} ) # ── Repr ────────────────────────────────────────────────────────────────── def test_repr(store: StateStore) -> None: store.set("k", "v") store.watch("k", lambda a, b, c: None) r = repr(store) assert "StateStore" in r assert "keys=1" in r