fleet-memory/hindsight-integrations/claude-code/tests/test_bank.py
Nicolò Boschi 35b2cbb6ed
fix(claude-code): fix plugin installation, config UX, and release workflow (#661)
* fix(claude-code): fix plugin installation and release workflow

- Fix plugin.json author field (string → object) to pass claude plugin validate
- Add hindsight-integrations/.claude-plugin/marketplace.json so users can install
  via: claude plugin marketplace add vectorize-io/hindsight --sparse hindsight-integrations
- Update README and install.sh with correct two-command install flow
- Fix release-integration.yml: add explicit package.json check for typescript type
  and add plugin type for integrations with neither pyproject.toml nor package.json
  (prevents claude-code from incorrectly falling into the typescript build path)
- Add CHANGELOG.md for the claude-code integration

* remove install.sh — users install via claude plugin commands directly

* test(claude-code): add 116 unit tests for plugin hooks and lib modules

* feat(claude-code): user settings.json at CLAUDE_PLUGIN_DATA for stable config

Plugin now checks CLAUDE_PLUGIN_DATA/settings.json after the versioned
plugin default, giving users a path that persists across updates:
  ~/.claude/plugins/data/hindsight-memory-hindsight/settings.json

Loading order: defaults → plugin settings.json → user settings.json → env vars

* fix(claude-code): use ~/.hindsight/claude-code.json for user config

Matches the ~/.openclaw/openclaw.json convention. Removes the confusing
CLAUDE_PLUGIN_DATA path whose name depends on marketplace+plugin identifiers.

* docs(claude-code): add ToS hint for claude-code LLM provider option

* fix(claude-code): set author to Hindsight Team in plugin.json

* ci: add test-claude-code-integration job to run plugin unit tests
2026-03-23 15:15:16 +01:00

136 lines
5.2 KiB
Python

"""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