"""Tests for lib/bank.py — bank ID derivation and mission management.""" import json import urllib.parse from unittest.mock import MagicMock import pytest from lib.bank import derive_bank_id, ensure_bank_mission def _cfg(**overrides): base = { "dynamicBankId": False, "bankId": "claude-code", "bankIdPrefix": "", "agentName": "claude-code", "dynamicBankGranularity": ["agent", "project"], "bankMission": "", "retainMission": None, } base.update(overrides) return base def _hook(session_id="sess-1", cwd="/home/user/myproject"): return {"session_id": session_id, "cwd": cwd} class TestDeriveBankIdStatic: def test_static_default_bank(self): assert derive_bank_id(_hook(), _cfg()) == "claude-code" def test_static_custom_bank_id(self): cfg = _cfg(bankId="my-agent") assert derive_bank_id(_hook(), cfg) == "my-agent" def test_static_with_prefix(self): cfg = _cfg(bankId="bot", bankIdPrefix="prod") assert derive_bank_id(_hook(), cfg) == "prod-bot" def test_static_prefix_without_bankid_uses_default(self): cfg = _cfg(bankId=None, bankIdPrefix="dev") assert derive_bank_id(_hook(), cfg) == "dev-claude-code" class TestDeriveBankIdDynamic: def test_dynamic_agent_project(self): cfg = _cfg(dynamicBankId=True, agentName="mybot", dynamicBankGranularity=["agent", "project"]) result = derive_bank_id(_hook(cwd="/home/user/hindsight"), cfg) assert result == "mybot::hindsight" def test_dynamic_url_encodes_special_chars(self): cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["project"]) result = derive_bank_id(_hook(cwd="/home/user/my project"), cfg) assert "my%20project" in result def test_dynamic_session_field(self): cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["session"]) result = derive_bank_id(_hook(session_id="abc-123"), cfg) assert "abc-123" in result def test_dynamic_with_prefix(self): cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["agent"], bankIdPrefix="v2") result = derive_bank_id(_hook(), cfg) assert result.startswith("v2-") def test_dynamic_channel_from_env(self, monkeypatch): monkeypatch.setenv("HINDSIGHT_CHANNEL_ID", "telegram-123") cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["channel"]) result = derive_bank_id(_hook(), cfg) assert "telegram-123" in result def test_dynamic_user_from_env(self, monkeypatch): monkeypatch.setenv("HINDSIGHT_USER_ID", "user-456") cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["user"]) result = derive_bank_id(_hook(), cfg) assert "user-456" in result def test_dynamic_missing_env_uses_defaults(self, monkeypatch): monkeypatch.delenv("HINDSIGHT_CHANNEL_ID", raising=False) monkeypatch.delenv("HINDSIGHT_USER_ID", raising=False) cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["channel", "user"]) result = derive_bank_id(_hook(), cfg) assert "default" in result assert "anonymous" in result def test_dynamic_empty_cwd_uses_unknown(self): cfg = _cfg(dynamicBankId=True, dynamicBankGranularity=["project"]) result = derive_bank_id({"session_id": "s", "cwd": ""}, cfg) assert "unknown" in result class TestEnsureBankMission: def test_sets_mission_on_first_call(self, state_dir): client = MagicMock() cfg = _cfg(bankMission="You are a helpful assistant.", bankId="test-bank") ensure_bank_mission(client, "test-bank", cfg) client.set_bank_mission.assert_called_once_with( "test-bank", "You are a helpful assistant.", retain_mission=None, timeout=10 ) def test_skips_if_already_set(self, state_dir): client = MagicMock() cfg = _cfg(bankMission="mission text") ensure_bank_mission(client, "bank-a", cfg) ensure_bank_mission(client, "bank-a", cfg) # second call assert client.set_bank_mission.call_count == 1 def test_skips_if_mission_empty(self, state_dir): client = MagicMock() cfg = _cfg(bankMission="") ensure_bank_mission(client, "bank-b", cfg) client.set_bank_mission.assert_not_called() def test_includes_retain_mission_if_set(self, state_dir): client = MagicMock() cfg = _cfg(bankMission="reflect mission", retainMission="retain mission") ensure_bank_mission(client, "bank-c", cfg) client.set_bank_mission.assert_called_once_with( "bank-c", "reflect mission", retain_mission="retain mission", timeout=10 ) def test_graceful_on_api_error(self, state_dir): client = MagicMock() client.set_bank_mission.side_effect = RuntimeError("server down") cfg = _cfg(bankMission="mission") # Should not raise ensure_bank_mission(client, "bank-d", cfg) def test_different_banks_each_set_once(self, state_dir): client = MagicMock() cfg = _cfg(bankMission="mission") ensure_bank_mission(client, "bank-x", cfg) ensure_bank_mission(client, "bank-y", cfg) assert client.set_bank_mission.call_count == 2