Newer
Older
navi-1 / tests / unit / profiles / test_profile_loader.py
"""load_profiles_from_dir — and the tally that keeps a partial set from being silent.

A profile whose config.json is unreadable is skipped by design (it must not take
the server down), but the UI then just shows a shorter list. On prod, 2026-10-07
15:20, four of them (modeler_3d, navi_code, secretary, server_admin) were dropped
with `Extra data: line 125 column 1` — a second JSON document appended to the
file — and the only trace was a per-profile error line: nothing said that four
profiles had gone missing.
"""

import json

from structlog.testing import capture_logs

from navi.profiles.loader import load_profiles_from_dir

GOOD_CONFIG = {"id": "good", "name": "Good", "description": "a valid profile"}


def write_profile(base, dirname, config=GOOD_CONFIG, prompt="You are helpful.\n"):
    """Create a profile directory; `config` is written raw as JSON text."""
    d = base / dirname
    d.mkdir()
    (d / "config.json").write_text(
        config if isinstance(config, str) else json.dumps(config), encoding="utf-8"
    )
    (d / "system_prompt.txt").write_text(prompt, encoding="utf-8")
    return d


class TestLoading:
    def test_valid_profiles_load(self, tmp_path):
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})
        write_profile(tmp_path, "beta", {**GOOD_CONFIG, "id": "beta"})

        profiles = load_profiles_from_dir(tmp_path)

        assert [p.id for p in profiles] == ["alpha", "beta"]

    def test_a_broken_config_does_not_take_the_others_with_it(self, tmp_path, capfd):
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})
        write_profile(tmp_path, "corrupt", "{ not json at all")

        profiles = load_profiles_from_dir(tmp_path)

        assert [p.id for p in profiles] == ["alpha"]
        out = capfd.readouterr().out
        assert "profile.loader.error" in out
        assert "corrupt" in out

    def test_a_config_missing_required_keys_is_skipped(self, tmp_path):
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})
        write_profile(tmp_path, "nameless", {"description": "no id, no name"})

        profiles = load_profiles_from_dir(tmp_path)

        assert [p.id for p in profiles] == ["alpha"]

    def test_subdirectories_without_the_files_are_not_dropped_profiles(self, tmp_path):
        """Stray directories are not corruption — counting them would make the
        tally cry wolf on every ordinary tree (e.g. a __pycache__ dir)."""
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})
        (tmp_path / "__pycache__").mkdir()
        (tmp_path / "notes.txt").write_text("not a profile", encoding="utf-8")

        profiles = load_profiles_from_dir(tmp_path)

        assert [p.id for p in profiles] == ["alpha"]


class TestSummaryLog:
    def test_the_tally_names_what_was_skipped(self, tmp_path):
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})
        write_profile(tmp_path, "corrupt", "{ definitely not json")

        with capture_logs() as captured:
            load_profiles_from_dir(tmp_path)

        errors = [e for e in captured if e["event"] == "profile.loader.error"]
        assert [e["profile_dir"] for e in errors] == ["corrupt"]

        summary = [e for e in captured if e["event"] == "profile.loader.summary"]
        assert len(summary) == 1
        assert summary[0]["log_level"] == "warning"
        assert summary[0]["loaded"] == 1
        assert summary[0]["skipped"] == ["corrupt"]

    def test_a_clean_load_warns_about_nothing(self, tmp_path):
        write_profile(tmp_path, "alpha", {**GOOD_CONFIG, "id": "alpha"})

        with capture_logs() as captured:
            load_profiles_from_dir(tmp_path)

        assert [e for e in captured if e["log_level"] in ("warning", "error")] == []