import os import sys PLUGIN_ROOT = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, PLUGIN_ROOT) sys.path.insert(0, os.environ.get("HERMES_ROOT", "/opt/hermes")) from platform_config import ( ChattoConfiguration, _get_env_or_extra_int, _get_env_or_extra_list, _get_env_or_extra_str, _get_env_or_extra_str_opt, _get_env_or_extra_truthy, _split_str_to_list, ) class TestPlatformConfigHelpers: """Test Chatto configuration helper functions.""" def test_get_env_or_extra_str_prefers_env(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", " env-value ") assert _get_env_or_extra_str("CHATTO_TEST", "extra-value") == "env-value" def test_get_env_or_extra_str_uses_extra_when_no_env(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_str("CHATTO_TEST", " extra-value ") == "extra-value" def test_get_env_or_extra_str_returns_empty_when_missing(self, monkeypatch): """The non-optional helper coerces a missing value to "" — use _get_env_or_extra_str_opt() when None has to stay distinguishable.""" monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_str("CHATTO_TEST", None) == "" def test_get_env_or_extra_str_opt_returns_none_when_missing(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_str_opt("CHATTO_TEST", None) is None def test_get_env_or_extra_str_opt_prefers_env(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", " env-value ") assert _get_env_or_extra_str_opt("CHATTO_TEST", "extra-value") == "env-value" def test_get_env_or_extra_truthy_parses_truthy_values(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", " yes ") assert _get_env_or_extra_truthy("CHATTO_TEST", None) is True def test_get_env_or_extra_truthy_uses_extra_when_no_env(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", " true ") is True def test_get_env_or_extra_truthy_uses_default_false(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", "false") is False def test_get_env_or_extra_truthy_default_true_when_none_and_no_extra( self, monkeypatch ): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", None, default=True) is True def test_get_env_or_extra_truthy_default_false_when_none_and_no_extra( self, monkeypatch ): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", None, default=False) is False def test_get_env_or_extra_truthy_explicit_bool_true(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", True, default=False) is True def test_get_env_or_extra_truthy_explicit_bool_false(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_truthy("CHATTO_TEST", False, default=True) is False def test_split_str_to_list_handles_comma_separated_values(self): assert _split_str_to_list("a,b, c ,d") == ["a", "b", "c", "d"] def test_get_env_or_extra_list_prefers_env(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", " one,two , three") assert _get_env_or_extra_list("CHATTO_TEST", ["ignored", "list"]) == [ "one", "two", "three", ] def test_get_env_or_extra_list_uses_extra_list(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_list("CHATTO_TEST", ["a", " b "]) == ["a", "b"] def test_get_env_or_extra_list_uses_extra_string(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_list("CHATTO_TEST", "x,y") == ["x", "y"] def test_get_env_or_extra_list_returns_empty_for_none(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_list("CHATTO_TEST", None) == [] def test_get_env_or_extra_int_prefers_env(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", " 45 ") assert _get_env_or_extra_int("CHATTO_TEST", 300, default=300) == 45 def test_get_env_or_extra_int_uses_extra_when_no_env(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_int("CHATTO_TEST", 60, default=300) == 60 def test_get_env_or_extra_int_parses_extra_string(self, monkeypatch): """config.yaml values arrive as strings when set via env-style files.""" monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_int("CHATTO_TEST", "90", default=300) == 90 def test_get_env_or_extra_int_falls_back_to_default_for_junk(self, monkeypatch): monkeypatch.setenv("CHATTO_TEST", "soon-ish") assert _get_env_or_extra_int("CHATTO_TEST", None, default=300) == 300 def test_get_env_or_extra_int_treats_yaml_bool_as_missing(self, monkeypatch): """A YAML true/false is not a number — silently take the default.""" monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_int("CHATTO_TEST", True, default=300) == 300 def test_get_env_or_extra_int_defaults_when_both_missing(self, monkeypatch): monkeypatch.delenv("CHATTO_TEST", raising=False) assert _get_env_or_extra_int("CHATTO_TEST", None, default=300) == 300 class TestMentionRoomFields: """The mention lists derive env names from their config keys and resolve to empty lists when nothing is set — channels are opt-in.""" def test_env_names_derive_from_the_config_keys(self): assert ( ChattoConfiguration.require_mention_rooms.env_name == "CHATTO_REQUIRE_MENTION_ROOMS" ) assert ( ChattoConfiguration.optional_mention_rooms.env_name == "CHATTO_OPTIONAL_MENTION_ROOMS" ) def test_lists_default_to_empty_without_any_configuration(self, monkeypatch): for var in ( "CHATTO_REQUIRE_MENTION_ROOMS", "CHATTO_OPTIONAL_MENTION_ROOMS", ): monkeypatch.delenv(var, raising=False) cfg = ChattoConfiguration( pconfig=PlatformConfigStub(extra={"base_url": "https://chat.test"}) ) assert cfg.require_mention_rooms.value == [] assert cfg.optional_mention_rooms.value == [] def test_lists_resolve_from_env_comma_separated(self, monkeypatch): monkeypatch.setenv("CHATTO_REQUIRE_MENTION_ROOMS", " room-1 , room-2 ") cfg = ChattoConfiguration( pconfig=PlatformConfigStub(extra={"base_url": "https://chat.test"}) ) assert cfg.require_mention_rooms.value == ["room-1", "room-2"] class TestExplicitOrigin: """`explicit` marks a value the user actually set, so the tool fallback can tell their intent from a default. A blank value is a placeholder — treating it as intent would block the profile-.env fallback it exists to enable.""" def test_env_value_is_explicit(self, monkeypatch): monkeypatch.setenv("CHATTO_TOKEN", "cht_x") cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) assert cfg.token.explicit is True def test_blank_env_value_is_not_explicit(self, monkeypatch): monkeypatch.setenv("CHATTO_TOKEN", "") cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) assert cfg.token.explicit is False def test_blank_extra_value_is_not_explicit(self): cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={"base_url": ""})) assert cfg.base_url.explicit is False def test_default_value_is_not_explicit(self, monkeypatch): monkeypatch.delenv("CHATTO_BASE_URL", raising=False) cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) assert cfg.base_url.explicit is False def test_user_set_default_value_is_explicit(self, monkeypatch): """A user may deliberately set base_url to the default string — the value alone cannot say so, which is what this flag records.""" monkeypatch.delenv("CHATTO_BASE_URL", raising=False) cfg = ChattoConfiguration( pconfig=PlatformConfigStub( extra={"base_url": ChattoConfiguration.base_url.default} ) ) assert cfg.base_url.explicit is True def test_explicit_false_bool_is_explicit(self, monkeypatch): """`false` is a real user choice, not a blank: the var being present is what records it, since the value alone says nothing.""" monkeypatch.setenv("CHATTO_ALLOW_ALL_USERS", "false") cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) assert cfg.allow_all_users.value is False assert cfg.allow_all_users.explicit is True class TestProfileSecretOverlay: """The tool layer re-reads the profile's own .env for values the process environment no longer carries — explicit user input must survive that.""" def test_default_yields_to_a_profile_secret(self): cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) cfg.overlay_profile_secrets({"CHATTO_TOKEN": "from-profile"}) assert cfg.token.value == "from-profile" def test_explicit_value_survives_the_overlay(self): cfg = ChattoConfiguration( pconfig=PlatformConfigStub(extra={"token": "from-config-yaml"}) ) cfg.overlay_profile_secrets({"CHATTO_TOKEN": "from-profile"}) assert cfg.token.value == "from-config-yaml" def test_blank_placeholder_yields_to_a_profile_secret(self): """`CHATTO_TOKEN=` left behind by the installer is not intent.""" cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={"token": ""})) cfg.overlay_profile_secrets({"CHATTO_TOKEN": "from-profile"}) assert cfg.token.value == "from-profile" def test_absent_profile_secret_leaves_the_value_alone(self): cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) cfg.overlay_profile_secrets({}) assert cfg.token.value is None def test_list_field_is_split_like_a_list_from_the_environment(self, monkeypatch): """The .env holds a comma-separated string, but a list field must hold a list: the tool gate concatenates the two lists and does `room_id in listed`, which on two strings is a substring test.""" monkeypatch.delenv("CHATTO_REQUIRE_MENTION_ROOMS", raising=False) cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) cfg.overlay_profile_secrets({"CHATTO_REQUIRE_MENTION_ROOMS": "room-1, room-2"}) assert cfg.require_mention_rooms.value == ["room-1", "room-2"] def test_int_field_is_parsed_like_an_int_from_the_environment(self, monkeypatch): monkeypatch.delenv("CHATTO_EDIT_WINDOW", raising=False) cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) cfg.overlay_profile_secrets({"CHATTO_EDIT_WINDOW": "600"}) assert cfg.edit_window.value == 600 def test_bool_field_is_parsed_like_a_bool_from_the_environment(self, monkeypatch): """`"false"` is a truthy string — a bool field left unparsed would turn an explicit opt-out into an opt-in.""" monkeypatch.delenv("CHATTO_REACTIONS", raising=False) cfg = ChattoConfiguration(pconfig=PlatformConfigStub(extra={})) cfg.overlay_profile_secrets({"CHATTO_REACTIONS": "false"}) assert cfg.reactions.value is False class PlatformConfigStub: """Just the attribute ChattoConfiguration reads off a PlatformConfig.""" def __init__(self, extra): self.extra = extra