* 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
94 lines
2.5 KiB
Python
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
|