.....FFFFF.FFFF..                                                        [100%]
=================================== FAILURES ===================================
______________ test_unusable_config_is_preserved_and_recovers[[]] ______________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unusable_config_is_preser0')
payload = b'[]'

    @pytest.mark.parametrize("payload", [b"[]", b"null", b'"text"', b"23", b"\xff", b"{bad json"])
    def test_unusable_config_is_preserved_and_recovers(store, payload):
        path = store / "board.json"
        path.write_bytes(payload)
>       assert config.load()["boards"]
               ^^^^^^^^^^^^^

tests/test_config_safety.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:316: in load
    return _clean(json.loads(path.read_text()))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = []

    def _clean(data: dict) -> dict[str, Any]:
>       priority = data.get("priority")
                   ^^^^^^^^
E       AttributeError: 'list' object has no attribute 'get'

src/suur_things_mcp/config.py:249: AttributeError
_____________ test_unusable_config_is_preserved_and_recovers[null] _____________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unusable_config_is_preser1')
payload = b'null'

    @pytest.mark.parametrize("payload", [b"[]", b"null", b'"text"', b"23", b"\xff", b"{bad json"])
    def test_unusable_config_is_preserved_and_recovers(store, payload):
        path = store / "board.json"
        path.write_bytes(payload)
>       assert config.load()["boards"]
               ^^^^^^^^^^^^^

tests/test_config_safety.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:316: in load
    return _clean(json.loads(path.read_text()))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = None

    def _clean(data: dict) -> dict[str, Any]:
>       priority = data.get("priority")
                   ^^^^^^^^
E       AttributeError: 'NoneType' object has no attribute 'get'

src/suur_things_mcp/config.py:249: AttributeError
____________ test_unusable_config_is_preserved_and_recovers["text"] ____________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unusable_config_is_preser2')
payload = b'"text"'

    @pytest.mark.parametrize("payload", [b"[]", b"null", b'"text"', b"23", b"\xff", b"{bad json"])
    def test_unusable_config_is_preserved_and_recovers(store, payload):
        path = store / "board.json"
        path.write_bytes(payload)
>       assert config.load()["boards"]
               ^^^^^^^^^^^^^

tests/test_config_safety.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:316: in load
    return _clean(json.loads(path.read_text()))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = 'text'

    def _clean(data: dict) -> dict[str, Any]:
>       priority = data.get("priority")
                   ^^^^^^^^
E       AttributeError: 'str' object has no attribute 'get'

src/suur_things_mcp/config.py:249: AttributeError
______________ test_unusable_config_is_preserved_and_recovers[23] ______________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unusable_config_is_preser3')
payload = b'23'

    @pytest.mark.parametrize("payload", [b"[]", b"null", b'"text"', b"23", b"\xff", b"{bad json"])
    def test_unusable_config_is_preserved_and_recovers(store, payload):
        path = store / "board.json"
        path.write_bytes(payload)
>       assert config.load()["boards"]
               ^^^^^^^^^^^^^

tests/test_config_safety.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:316: in load
    return _clean(json.loads(path.read_text()))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = 23

    def _clean(data: dict) -> dict[str, Any]:
>       priority = data.get("priority")
                   ^^^^^^^^
E       AttributeError: 'int' object has no attribute 'get'

src/suur_things_mcp/config.py:249: AttributeError
_____________ test_unusable_config_is_preserved_and_recovers[\xff] _____________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unusable_config_is_preser4')
payload = b'\xff'

    @pytest.mark.parametrize("payload", [b"[]", b"null", b'"text"', b"23", b"\xff", b"{bad json"])
    def test_unusable_config_is_preserved_and_recovers(store, payload):
        path = store / "board.json"
        path.write_bytes(payload)
>       assert config.load()["boards"]
               ^^^^^^^^^^^^^

tests/test_config_safety.py:50: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:316: in load
    return _clean(json.loads(path.read_text()))
                             ^^^^^^^^^^^^^^^^
../../../.local/share/uv/python/cpython-3.14.2-macos-aarch64-none/lib/python3.14/pathlib/__init__.py:788: in read_text
    return f.read()
           ^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <encodings.utf_8.IncrementalDecoder object at 0x1077c03b0>
input = b'\xff', final = True

>   ???
E   UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

<frozen codecs>:325: UnicodeDecodeError
__________ test_recovery_does_not_overwrite_distinct_corrupt_configs ___________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_recovery_does_not_overwri0')

    def test_recovery_does_not_overwrite_distinct_corrupt_configs(store):
        path = store / "board.json"
        for payload in [b"{broken-one", b"{broken-two"]:
            path.write_bytes(payload)
            config.load()
>       assert {p.read_bytes() for p in store.glob("board.json.corrupt-*")} == {b"{broken-one", b"{broken-two"}
E       AssertionError: assert {b'{broken-two'} == {b'{broken-on...'{broken-two'}
E         
E         Extra items in the right set:
E         b'{broken-one'
E         Use -v to get more diff

tests/test_config_safety.py:65: AssertionError
________________ test_unreadable_config_does_not_silently_reset ________________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_unreadable_config_does_no0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x1071da090>

    def test_unreadable_config_does_not_silently_reset(store, monkeypatch):
        config.save({"prefs": {"editor": "user-editor"}})
        original = (store / "board.json").read_bytes()
        def denied(*args, **kwargs):
            raise PermissionError("not readable")
        monkeypatch.setattr(type(store), "read_bytes", denied)
>       with pytest.raises(PermissionError):
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       Failed: DID NOT RAISE PermissionError

tests/test_config_safety.py:74: Failed
____________ test_junk_repo_lists_are_ignored_without_crashing[42] _____________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_junk_repo_lists_are_ignor0')
repos = 42

    @pytest.mark.parametrize("repos", [42, True, {"repo": "bad"}, "bad"])
    def test_junk_repo_lists_are_ignored_without_crashing(store, repos):
>       cfg = config.save({"links": {"x": {"repos": repos}}, "prefs": {"editor": "code"}})
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_config_safety.py:82: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:334: in save
    cfg = _clean(data or {})
          ^^^^^^^^^^^^^^^^^^
src/suur_things_mcp/config.py:255: in _clean
    link_table = _clean_links(data)
                 ^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = {'links': {'x': {'repos': 42}}, 'prefs': {'editor': 'code'}}

    def _clean_links(data: dict) -> dict[str, Any]:
        """Normalize the link table. Each item (project/area) holds a LIST of repos.
    
        Accepts the new shape ({"repos": [...]}) and a legacy single-repo entry; repos
        are de-duplicated by normalized path (last write wins).
        """
        raw = data.get("links")
        if not isinstance(raw, dict):
            return {}
        out: dict[str, Any] = {}
        for key, entry in raw.items():
            if not isinstance(entry, dict):
                continue
            kind = entry.get("kind") if entry.get("kind") in ("project", "area") else "project"
            raw_repos = entry.get("repos")
            if not isinstance(raw_repos, list) and entry.get("repo"):  # legacy single repo
                raw_repos = [{"repo": entry.get("repo"), "github": entry.get("github"), "label": entry.get("label")}]
            by_path: dict[str, dict] = {}
>           for r in raw_repos or []:
                     ^^^^^^^^^^^^^^^
E           TypeError: 'int' object is not iterable

src/suur_things_mcp/config.py:94: TypeError
___________ test_junk_repo_lists_are_ignored_without_crashing[True] ____________

store = PosixPath('/private/var/folders/vs/33_lmwh911z8h4ny9xzhdy5w0000gn/T/pytest-of-artyomsklyarov/pytest-5/test_junk_repo_lists_are_ignor1')
repos = True

    @pytest.mark.parametrize("repos", [42, True, {"repo": "bad"}, "bad"])
    def test_junk_repo_lists_are_ignored_without_crashing(store, repos):
>       cfg = config.save({"links": {"x": {"repos": repos}}, "prefs": {"editor": "code"}})
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_config_safety.py:82: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/suur_things_mcp/config.py:334: in save
    cfg = _clean(data or {})
          ^^^^^^^^^^^^^^^^^^
src/suur_things_mcp/config.py:255: in _clean
    link_table = _clean_links(data)
                 ^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

data = {'links': {'x': {'repos': True}}, 'prefs': {'editor': 'code'}}

    def _clean_links(data: dict) -> dict[str, Any]:
        """Normalize the link table. Each item (project/area) holds a LIST of repos.
    
        Accepts the new shape ({"repos": [...]}) and a legacy single-repo entry; repos
        are de-duplicated by normalized path (last write wins).
        """
        raw = data.get("links")
        if not isinstance(raw, dict):
            return {}
        out: dict[str, Any] = {}
        for key, entry in raw.items():
            if not isinstance(entry, dict):
                continue
            kind = entry.get("kind") if entry.get("kind") in ("project", "area") else "project"
            raw_repos = entry.get("repos")
            if not isinstance(raw_repos, list) and entry.get("repo"):  # legacy single repo
                raw_repos = [{"repo": entry.get("repo"), "github": entry.get("github"), "label": entry.get("label")}]
            by_path: dict[str, dict] = {}
>           for r in raw_repos or []:
                     ^^^^^^^^^^^^^^^
E           TypeError: 'bool' object is not iterable

src/suur_things_mcp/config.py:94: TypeError
=========================== short test summary info ============================
FAILED tests/test_config_safety.py::test_unusable_config_is_preserved_and_recovers[[]]
FAILED tests/test_config_safety.py::test_unusable_config_is_preserved_and_recovers[null]
FAILED tests/test_config_safety.py::test_unusable_config_is_preserved_and_recovers["text"]
FAILED tests/test_config_safety.py::test_unusable_config_is_preserved_and_recovers[23]
FAILED tests/test_config_safety.py::test_unusable_config_is_preserved_and_recovers[\xff]
FAILED tests/test_config_safety.py::test_recovery_does_not_overwrite_distinct_corrupt_configs
FAILED tests/test_config_safety.py::test_unreadable_config_does_not_silently_reset
FAILED tests/test_config_safety.py::test_junk_repo_lists_are_ignored_without_crashing[42]
FAILED tests/test_config_safety.py::test_junk_repo_lists_are_ignored_without_crashing[True]
9 failed, 8 passed in 0.32s
