fleet-memory/hindsight-integrations/claude-code/tests/conftest.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

94 lines
2.5 KiB
Python

"""Shared fixtures for Hindsight Claude Code plugin tests."""
import io
import json
import os
import sys
import tempfile
import pytest
# Make scripts/ importable as the root — the hook scripts do:
# sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# so lib.* imports resolve relative to scripts/
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
if SCRIPTS_DIR not in sys.path:
sys.path.insert(0, os.path.abspath(SCRIPTS_DIR))
@pytest.fixture()
def state_dir(tmp_path, monkeypatch):
"""Isolated state directory — prevents tests from touching real state files."""
d = tmp_path / "state"
d.mkdir()
monkeypatch.setenv("CLAUDE_PLUGIN_DATA", str(tmp_path))
return d
@pytest.fixture()
def plugin_root(tmp_path):
"""Temp plugin root with a minimal settings.json."""
settings = tmp_path / "settings.json"
settings.write_text(json.dumps({}))
return tmp_path
@pytest.fixture()
def default_config(plugin_root, monkeypatch):
"""Load config with no overrides, isolated from real settings.json."""
monkeypatch.setenv("CLAUDE_PLUGIN_ROOT", str(plugin_root))
# Strip any real HINDSIGHT_* env vars that might bleed in
for key in list(os.environ):
if key.startswith("HINDSIGHT_"):
monkeypatch.delenv(key, raising=False)
from lib.config import load_config
return load_config()
def make_hook_input(
prompt="What is the capital of France?",
session_id="sess-abc123",
cwd="/home/user/myproject",
transcript_path="",
):
return {
"prompt": prompt,
"session_id": session_id,
"cwd": cwd,
"transcript_path": transcript_path,
}
def make_transcript_file(tmp_path, messages):
"""Write messages as a JSONL transcript file (flat test format)."""
f = tmp_path / "transcript.jsonl"
lines = [json.dumps(m) for m in messages]
f.write_text("\n".join(lines))
return str(f)
def make_recall_response(memories):
"""Build a fake /recall API response."""
return {"results": memories}
def make_memory(text, mem_type="experience", mentioned_at="2024-01-15"):
return {"text": text, "type": mem_type, "mentioned_at": mentioned_at}
class FakeHTTPResponse:
"""Minimal urllib response mock."""
def __init__(self, data: dict, status: int = 200):
self.status = status
self._data = json.dumps(data).encode()
def read(self):
return self._data
def __enter__(self):
return self
def __exit__(self, *_):
pass