@@ -1629,3 +1629,115 @@ def test_merge_tier_fields_no_llm_provider_returns_heuristic_only():
16291629 assert res ["agent_persona_names" ] == []
16301630 assert res ["user_name" ] is None
16311631 assert res ["primary_platform" ] is None
1632+
1633+
1634+ # ─────────────────────────────────────────────────────────────────────────
1635+ # External-API privacy warning (issue #24).
1636+ #
1637+ # When mempalace init resolves an LLM provider whose endpoint will send
1638+ # user content off the local machine/network, init MUST print a clear
1639+ # warning naming the provider, stating that MemPalace doesn't control
1640+ # how the provider logs/retains/uses the data, and pointing at --no-llm.
1641+ # Local providers (Ollama on localhost, LM Studio on LAN, etc.) MUST NOT
1642+ # trigger the warning.
1643+ # ─────────────────────────────────────────────────────────────────────────
1644+
1645+
1646+ def test_init_prints_privacy_warning_when_provider_is_external (
1647+ ai_dialogue_corpus : Path , tmp_path : Path , capsys
1648+ ):
1649+ """When cmd_init successfully acquires a provider whose
1650+ is_external_service is True, output must contain the privacy
1651+ warning text including the EXTERNAL marker.
1652+ """
1653+ from mempalace .cli import cmd_init
1654+
1655+ palace = tmp_path / "palace"
1656+ args = _init_args (ai_dialogue_corpus ) # default = LLM ON
1657+
1658+ fake_provider = MagicMock ()
1659+ fake_provider .check_available .return_value = (True , "ok" )
1660+ fake_provider .is_external_service = True
1661+ fake_provider .classify .return_value = MagicMock (text = '{"classifications": []}' )
1662+
1663+ with (
1664+ patch ("mempalace.cli.MempalaceConfig" , return_value = _stub_cfg (palace )),
1665+ patch ("mempalace.cli.get_provider" , return_value = fake_provider ),
1666+ patch ("mempalace.cli._maybe_run_mine_after_init" ),
1667+ patch ("mempalace.room_detector_local.detect_rooms_local" ),
1668+ ):
1669+ cmd_init (args )
1670+
1671+ out = capsys .readouterr ().out
1672+ assert "EXTERNAL API" in out , (
1673+ f"Privacy warning must mention 'EXTERNAL API' when provider is external. " f"Got: { out !r} "
1674+ )
1675+ assert (
1676+ "--no-llm" in out
1677+ ), f"Privacy warning must point users at --no-llm to opt out. Got: { out !r} "
1678+ # The warning should also tell users MemPalace isn't responsible
1679+ # for downstream provider behavior.
1680+ assert (
1681+ "does not control" in out .lower ()
1682+ or "not responsible" in out .lower ()
1683+ or "logs" in out .lower ()
1684+ or "retains" in out .lower ()
1685+ ), (
1686+ f"Privacy warning must clarify MemPalace doesn't control how the "
1687+ f"provider handles the data. Got: { out !r} "
1688+ )
1689+
1690+
1691+ def test_init_no_privacy_warning_when_provider_is_local (
1692+ ai_dialogue_corpus : Path , tmp_path : Path , capsys
1693+ ):
1694+ """When cmd_init successfully acquires a LOCAL provider (e.g. Ollama
1695+ on localhost, LM Studio on LAN), the privacy warning MUST NOT fire —
1696+ nothing is leaving the user's machine/network.
1697+ """
1698+ from mempalace .cli import cmd_init
1699+
1700+ palace = tmp_path / "palace"
1701+ args = _init_args (ai_dialogue_corpus ) # default = LLM ON
1702+
1703+ fake_provider = MagicMock ()
1704+ fake_provider .check_available .return_value = (True , "ok" )
1705+ fake_provider .is_external_service = False # Local provider — no warning
1706+ fake_provider .classify .return_value = MagicMock (text = '{"classifications": []}' )
1707+
1708+ with (
1709+ patch ("mempalace.cli.MempalaceConfig" , return_value = _stub_cfg (palace )),
1710+ patch ("mempalace.cli.get_provider" , return_value = fake_provider ),
1711+ patch ("mempalace.cli._maybe_run_mine_after_init" ),
1712+ patch ("mempalace.room_detector_local.detect_rooms_local" ),
1713+ ):
1714+ cmd_init (args )
1715+
1716+ out = capsys .readouterr ().out
1717+ assert "EXTERNAL API" not in out , (
1718+ f"Privacy warning fired for a LOCAL provider — should not have. " f"Got: { out !r} "
1719+ )
1720+
1721+
1722+ def test_init_no_privacy_warning_with_no_llm_flag (ai_dialogue_corpus : Path , tmp_path : Path , capsys ):
1723+ """With --no-llm, no provider is acquired at all, so the privacy
1724+ warning has nothing to fire on. Output must not contain it.
1725+ """
1726+ from mempalace .cli import cmd_init
1727+
1728+ palace = tmp_path / "palace"
1729+ args = _init_args (ai_dialogue_corpus , no_llm = True )
1730+
1731+ with (
1732+ patch ("mempalace.cli.MempalaceConfig" , return_value = _stub_cfg (palace )),
1733+ patch ("mempalace.cli.get_provider" ) as mock_get ,
1734+ patch ("mempalace.cli._maybe_run_mine_after_init" ),
1735+ patch ("mempalace.room_detector_local.detect_rooms_local" ),
1736+ ):
1737+ cmd_init (args )
1738+
1739+ mock_get .assert_not_called (), "--no-llm must short-circuit before provider acquisition"
1740+ out = capsys .readouterr ().out
1741+ assert (
1742+ "EXTERNAL API" not in out
1743+ ), f"Privacy warning fired on --no-llm path — should not have. Got: { out !r} "
0 commit comments