Compare commits

...
7 changed files with 498 additions and 54 deletions
@@ -0,0 +1,109 @@
"""
Tests for configuration validation.
Verifies that config validation catches invalid parameter combinations.
"""
import os
import pytest
@pytest.fixture(autouse=True)
def setup_test_env():
"""Set up environment for each test, restoring original values after."""
from hindsight_api.config import clear_config_cache
# Save original environment values
env_vars_to_save = [
"HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS",
"HINDSIGHT_API_RETAIN_CHUNK_SIZE",
"HINDSIGHT_API_LLM_PROVIDER",
"HINDSIGHT_API_LLM_MODEL",
]
# Save original values
original_values = {}
for key in env_vars_to_save:
original_values[key] = os.environ.get(key)
clear_config_cache()
yield
# Restore original environment
for key, original_value in original_values.items():
if original_value is None:
os.environ.pop(key, None)
else:
os.environ[key] = original_value
clear_config_cache()
def test_retain_max_completion_tokens_must_be_greater_than_chunk_size():
"""Test that RETAIN_MAX_COMPLETION_TOKENS > RETAIN_CHUNK_SIZE validation works."""
from hindsight_api.config import HindsightConfig
# Set invalid config: max_completion_tokens <= chunk_size
os.environ["HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS"] = "1000"
os.environ["HINDSIGHT_API_RETAIN_CHUNK_SIZE"] = "2000"
os.environ["HINDSIGHT_API_LLM_PROVIDER"] = "mock"
# Should raise ValueError with helpful message
with pytest.raises(ValueError) as exc_info:
HindsightConfig.from_env()
error_message = str(exc_info.value)
# Verify error message contains helpful information
assert "HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS" in error_message
assert "1000" in error_message
assert "HINDSIGHT_API_RETAIN_CHUNK_SIZE" in error_message
assert "2000" in error_message
assert "must be greater than" in error_message
assert "You have two options to fix this:" in error_message
assert "Increase HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS" in error_message
assert "Use a model that supports" in error_message
def test_retain_max_completion_tokens_equal_to_chunk_size_fails():
"""Test that RETAIN_MAX_COMPLETION_TOKENS == RETAIN_CHUNK_SIZE also fails."""
from hindsight_api.config import HindsightConfig
# Set invalid config: max_completion_tokens == chunk_size
os.environ["HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS"] = "3000"
os.environ["HINDSIGHT_API_RETAIN_CHUNK_SIZE"] = "3000"
os.environ["HINDSIGHT_API_LLM_PROVIDER"] = "mock"
# Should raise ValueError
with pytest.raises(ValueError) as exc_info:
HindsightConfig.from_env()
error_message = str(exc_info.value)
assert "must be greater than" in error_message
def test_valid_retain_config_succeeds():
"""Test that valid config with max_completion_tokens > chunk_size works."""
from hindsight_api.config import HindsightConfig
# Set valid config: max_completion_tokens > chunk_size
os.environ["HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS"] = "64000"
os.environ["HINDSIGHT_API_RETAIN_CHUNK_SIZE"] = "3000"
os.environ["HINDSIGHT_API_LLM_PROVIDER"] = "mock"
# Should not raise
config = HindsightConfig.from_env()
assert config.retain_max_completion_tokens == 64000
assert config.retain_chunk_size == 3000
# Note: The BadRequestError wrapping is implemented in fact_extraction.py
# but requires a complex integration test setup. The functionality is
# straightforward: when a BadRequestError containing keywords like
# "max_tokens", "max_completion_tokens", or "maximum context" is caught,
# it's wrapped in a ValueError with helpful guidance.
#
# The config validation tests above ensure users get early feedback
# about invalid configurations before runtime errors occur.
+11 -3
View File
@@ -738,12 +738,20 @@ class TestMentalModelRefreshTagSecurity:
"Refreshed model should access memories/models with matching tags (user:alice)"
# MUST NOT include Bob's content (security violation)
assert "bob" not in refreshed_content and "python" not in refreshed_content and "tea" not in refreshed_content, \
f"SECURITY VIOLATION: Refreshed model accessed memories/models with different tags (user:bob). Content: {refreshed_content}"
# Use word boundary matching to avoid false positives (e.g., "team" contains "tea")
import re
def contains_word(text: str, word: str) -> bool:
"""Check if text contains word as a whole word (not substring)."""
return bool(re.search(rf'\b{re.escape(word)}\b', text, re.IGNORECASE))
assert not contains_word(refreshed_content, "bob") and \
not contains_word(refreshed_content, "python") and \
not contains_word(refreshed_content, "tea"), \
f"SECURITY VIOLATION: Refreshed model accessed memories/models with different tags (user:bob). Content: {refreshed['content']}"
# MUST NOT include untagged content (security violation)
assert "100 employees" not in refreshed_content and "growing fast" not in refreshed_content, \
f"SECURITY VIOLATION: Refreshed model accessed untagged memories/models. Content: {refreshed_content}"
f"SECURITY VIOLATION: Refreshed model accessed untagged memories/models. Content: {refreshed['content']}"
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
+32 -15
View File
@@ -86,21 +86,34 @@ def setup_logging(verbose: bool = False):
def load_config_file():
"""Load configuration from file if it exists."""
# Check both config file locations
config_files = [CONFIG_FILE, CONFIG_FILE_ALT]
for config_path in config_files:
if config_path.exists():
with open(config_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
# Handle 'export VAR=value' format
if line.startswith("export "):
line = line[7:]
key, value = line.split("=", 1)
if key not in os.environ: # Don't override env vars
os.environ[key] = value
"""Load configuration from the active profile's file if it exists.
IMPORTANT: Only loads from the active profile, never from default if a specific profile is set.
Uses dynamic path resolution to support testing with temporary HOME directories.
"""
from .profile_manager import ProfileManager, resolve_active_profile
# Resolve which profile to use (respects --profile flag, env vars, active_profile file)
active_profile = resolve_active_profile()
# Get the config file path for this profile
# Use ProfileManager which resolves paths dynamically
pm = ProfileManager()
paths = pm.resolve_profile_paths(active_profile)
config_path = paths.config
# Load ONLY this profile's config, never fall back to default
if config_path.exists():
with open(config_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
# Handle 'export VAR=value' format
if line.startswith("export "):
line = line[7:]
key, value = line.split("=", 1)
if key not in os.environ: # Don't override env vars
os.environ[key] = value
def get_config():
@@ -1156,6 +1169,10 @@ def main():
if global_profile == "default":
global_profile = None
# Set the CLI profile override so it's available to resolve_active_profile()
# This must happen BEFORE any config loading (load_config_file, get_config, etc.)
set_cli_profile_override(global_profile)
# Check for built-in commands first
# Find the first non-flag argument (the actual command)
command = None
@@ -108,6 +108,13 @@ class DaemonEmbedManager(EmbedManager):
daemon_log = paths.log
port = paths.port
# Load profile's .env file and merge with provided config
# This fixes issue #305 where profile env vars were ignored
profile_config = self._profile_manager.load_profile_config(profile)
# Merge: profile config first, then override with explicitly provided config
merged_config = {**profile_config, **config}
config = merged_config
# Build environment with LLM config
# Support both formats: simple keys ("llm_api_key") and env var format ("HINDSIGHT_API_LLM_API_KEY")
env = os.environ.copy()
@@ -65,9 +65,25 @@ class ProfileManager:
"""Initialize the profile manager."""
self._ensure_directories()
def _get_config_dir(self) -> Path:
"""Get config directory path dynamically (supports testing with temp HOME)."""
return Path.home() / ".hindsight"
def _get_profiles_dir(self) -> Path:
"""Get profiles directory path dynamically."""
return self._get_config_dir() / "profiles"
def _get_metadata_file(self) -> Path:
"""Get metadata file path dynamically."""
return self._get_profiles_dir() / "metadata.json"
def _get_active_profile_file(self) -> Path:
"""Get active profile file path dynamically."""
return self._get_config_dir() / "active_profile"
def _ensure_directories(self):
"""Ensure profile directories exist."""
PROFILES_DIR.mkdir(parents=True, exist_ok=True)
self._get_profiles_dir().mkdir(parents=True, exist_ok=True)
def list_profiles(self) -> list[ProfileInfo]:
"""List all profiles with their status.
@@ -80,7 +96,7 @@ class ProfileManager:
profiles = []
# Add default profile if config exists
default_config = CONFIG_DIR / "embed"
default_config = self._get_config_dir() / "embed"
if default_config.exists():
profiles.append(
ProfileInfo(
@@ -119,10 +135,10 @@ class ProfileManager:
"""
if not name:
# Default profile exists if config file exists
return (CONFIG_DIR / "embed").exists()
return (self._get_config_dir() / "embed").exists()
# Named profile exists if config file exists
config_path = PROFILES_DIR / f"{name}.env"
config_path = self._get_profiles_dir() / f"{name}.env"
return config_path.exists()
def get_profile(self, name: str) -> Optional[ProfileInfo]:
@@ -186,7 +202,7 @@ class ProfileManager:
port = self._allocate_port(name)
# Write config file
config_path = PROFILES_DIR / f"{name}.env"
config_path = self._get_profiles_dir() / f"{name}.env"
config_lines = [f"{key}={value}" for key, value in config.items()]
config_path.write_text("\n".join(config_lines) + "\n")
@@ -223,17 +239,17 @@ class ProfileManager:
raise ValueError(f"Profile '{name}' does not exist")
# Remove config file
config_path = PROFILES_DIR / f"{name}.env"
config_path = self._get_profiles_dir() / f"{name}.env"
if config_path.exists():
config_path.unlink()
# Remove lock file
lock_path = PROFILES_DIR / f"{name}.lock"
lock_path = self._get_profiles_dir() / f"{name}.lock"
if lock_path.exists():
lock_path.unlink()
# Remove log file
log_path = PROFILES_DIR / f"{name}.log"
log_path = self._get_profiles_dir() / f"{name}.log"
if log_path.exists():
log_path.unlink()
@@ -259,12 +275,13 @@ class ProfileManager:
if name and not self.profile_exists(name):
raise ValueError(f"Profile '{name}' does not exist")
active_file = self._get_active_profile_file()
if name:
ACTIVE_PROFILE_FILE.write_text(name)
active_file.write_text(name)
else:
# Clear active profile
if ACTIVE_PROFILE_FILE.exists():
ACTIVE_PROFILE_FILE.unlink()
if active_file.exists():
active_file.unlink()
def get_active_profile(self) -> str:
"""Get the currently active profile name.
@@ -272,8 +289,9 @@ class ProfileManager:
Returns:
Profile name, or empty string if no active profile.
"""
if ACTIVE_PROFILE_FILE.exists():
return ACTIVE_PROFILE_FILE.read_text().strip()
active_file = self._get_active_profile_file()
if active_file.exists():
return active_file.read_text().strip()
return ""
def resolve_profile_paths(self, name: str) -> ProfilePaths:
@@ -285,12 +303,16 @@ class ProfileManager:
Returns:
ProfilePaths with config, lock, log, and port.
"""
# Use dynamic path resolution to support testing with temporary HOME directories
config_dir = Path.home() / ".hindsight"
profiles_dir = config_dir / "profiles"
if not name:
# Default profile
return ProfilePaths(
config=CONFIG_DIR / "embed",
lock=CONFIG_DIR / "daemon.lock",
log=CONFIG_DIR / "daemon.log",
config=config_dir / "embed",
lock=config_dir / "daemon.lock",
log=config_dir / "daemon.log",
port=DEFAULT_PORT,
)
@@ -299,12 +321,60 @@ class ProfileManager:
port = metadata.profiles.get(name, {}).get("port", self._allocate_port(name))
return ProfilePaths(
config=PROFILES_DIR / f"{name}.env",
lock=PROFILES_DIR / f"{name}.lock",
log=PROFILES_DIR / f"{name}.log",
config=profiles_dir / f"{name}.env",
lock=profiles_dir / f"{name}.lock",
log=profiles_dir / f"{name}.log",
port=port,
)
def load_profile_config(self, name: str) -> dict[str, str]:
"""Load configuration from a profile's .env file.
Args:
name: Profile name (empty string for default).
Returns:
Dictionary of environment variable key-value pairs from the profile's .env file.
Also includes simple key aliases (e.g., 'idle_timeout' for 'HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT').
"""
paths = self.resolve_profile_paths(name)
config = {}
if not paths.config.exists():
return config
# Parse .env file
with open(paths.config) as f:
for line in f:
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith("#"):
continue
# Handle 'export VAR=value' format
if line.startswith("export "):
line = line[7:]
# Parse KEY=VALUE
if "=" in line:
key, value = line.split("=", 1)
config[key.strip()] = value.strip()
# Add simple key aliases for backward compatibility
# Some code checks config.get("idle_timeout") instead of the full env var name
key_aliases = {
"HINDSIGHT_API_LLM_API_KEY": "llm_api_key",
"HINDSIGHT_API_LLM_PROVIDER": "llm_provider",
"HINDSIGHT_API_LLM_MODEL": "llm_model",
"HINDSIGHT_API_LLM_BASE_URL": "llm_base_url",
"HINDSIGHT_API_LOG_LEVEL": "log_level",
"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT": "idle_timeout",
}
for env_key, simple_key in key_aliases.items():
if env_key in config and simple_key not in config:
config[simple_key] = config[env_key]
return config
def _allocate_port(self, name: str) -> int:
"""Allocate a port for a profile using hash-based strategy.
@@ -359,11 +429,12 @@ class ProfileManager:
Returns:
ProfileMetadata object.
"""
if not METADATA_FILE.exists():
metadata_file = self._get_metadata_file()
if not metadata_file.exists():
return ProfileMetadata()
try:
with open(METADATA_FILE) as f:
with open(metadata_file) as f:
data = json.load(f)
return ProfileMetadata(version=data.get("version", 1), profiles=data.get("profiles", {}))
except (json.JSONDecodeError, IOError) as e:
@@ -372,9 +443,9 @@ class ProfileManager:
file=sys.stderr,
)
# Backup corrupted metadata
backup_path = METADATA_FILE.with_suffix(".json.bak")
if METADATA_FILE.exists():
METADATA_FILE.rename(backup_path)
backup_path = metadata_file.with_suffix(".json.bak")
if metadata_file.exists():
metadata_file.rename(backup_path)
return ProfileMetadata()
def _save_metadata(self, metadata: ProfileMetadata):
@@ -386,7 +457,8 @@ class ProfileManager:
self._ensure_directories()
# Use atomic write with temp file
temp_file = METADATA_FILE.with_suffix(".json.tmp")
metadata_file = self._get_metadata_file()
temp_file = metadata_file.with_suffix(".json.tmp")
with open(temp_file, "w") as f:
# Acquire exclusive lock
@@ -403,7 +475,7 @@ class ProfileManager:
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
# Atomic rename
temp_file.rename(METADATA_FILE)
temp_file.rename(metadata_file)
def resolve_active_profile() -> str:
@@ -0,0 +1,223 @@
"""Test for validating that profile environment variables are loaded correctly when starting daemon.
This is a regression test for issue #305 where profile .env files were not loaded
before daemon startup, causing environment variables to be ignored.
"""
import json
from pathlib import Path
import pytest
@pytest.fixture
def temp_home(tmp_path, monkeypatch):
"""Create a temporary home directory."""
temp_home = tmp_path / "home"
temp_home.mkdir()
monkeypatch.setenv("HOME", str(temp_home))
return temp_home
def test_profile_config_is_loaded_for_daemon(temp_home):
"""Test that profile .env config is loaded when preparing daemon startup.
Before the fix, the daemon would ignore all values from the profile's .env file,
using only os.environ or hardcoded defaults. This caused HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT
and other profile-specific settings to be silently ignored.
"""
# Create a profile with custom configuration
profile_dir = temp_home / ".hindsight" / "profiles"
profile_dir.mkdir(parents=True, exist_ok=True)
profile_name = "test-timeout"
profile_env_path = profile_dir / f"{profile_name}.env"
# Write profile config with custom values
profile_env_path.write_text(
"HINDSIGHT_API_LLM_PROVIDER=openai\n"
"HINDSIGHT_API_LLM_API_KEY=sk-test-fake-key\n"
"HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n"
"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=0\n"
"HINDSIGHT_API_LOG_LEVEL=debug\n"
)
# Create metadata to register the profile with a port
metadata_path = profile_dir / "metadata.json"
metadata = {
"version": 1,
"profiles": {
profile_name: {
"port": 9876,
"created_at": "2024-01-01T00:00:00+00:00",
"last_used": "2024-01-01T00:00:00+00:00",
}
},
}
metadata_path.write_text(json.dumps(metadata, indent=2))
# Verify that ProfileManager can load the profile config
from hindsight_embed.profile_manager import ProfileManager
pm = ProfileManager()
# Verify profile exists
assert pm.profile_exists(profile_name)
# Get profile paths
paths = pm.resolve_profile_paths(profile_name)
assert paths.config.exists()
assert paths.config == profile_env_path
assert paths.port == 9876
# Load profile config (this simulates what the fix should do)
profile_config = pm.load_profile_config(profile_name)
# Verify config was loaded correctly
assert profile_config["HINDSIGHT_API_LLM_PROVIDER"] == "openai"
assert profile_config["HINDSIGHT_API_LLM_API_KEY"] == "sk-test-fake-key"
assert profile_config["HINDSIGHT_API_LLM_MODEL"] == "gpt-4o-mini"
assert profile_config["HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT"] == "0"
assert profile_config["HINDSIGHT_API_LOG_LEVEL"] == "debug"
# Verify that idle_timeout simple key is also available for backward compat
# (some code checks config.get("idle_timeout"))
assert profile_config.get("idle_timeout") == "0"
def test_load_config_file_uses_correct_profile(temp_home, monkeypatch):
"""Test that load_config_file() loads the correct profile and not default.
This is the core fix for issue #305 - when a profile is specified, we should
ONLY load that profile's .env, never the default profile's config.
"""
import os
# Clear any existing profile override state
from hindsight_embed.cli import set_cli_profile_override
set_cli_profile_override(None)
# Must clear HINDSIGHT_EMBED_PROFILE env var to ensure test isolation
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
from hindsight_embed.cli import load_config_file
from hindsight_embed.profile_manager import ProfileManager
# Create default profile with one provider
default_config_dir = temp_home / ".hindsight"
default_config_dir.mkdir(parents=True, exist_ok=True)
(default_config_dir / "embed").write_text(
"HINDSIGHT_API_LLM_PROVIDER=openai\n" "HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n"
)
# Create a named profile with a DIFFERENT provider
profile_dir = temp_home / ".hindsight" / "profiles"
profile_dir.mkdir(parents=True, exist_ok=True)
profile_name = "myapp"
profile_env_path = profile_dir / f"{profile_name}.env"
profile_env_path.write_text("HINDSIGHT_API_LLM_PROVIDER=groq\n" "HINDSIGHT_API_LLM_MODEL=llama-3.1-70b\n")
# Create metadata
import json
metadata_path = profile_dir / "metadata.json"
metadata = {
"version": 1,
"profiles": {
profile_name: {
"port": 9876,
"created_at": "2024-01-01T00:00:00+00:00",
"last_used": "2024-01-01T00:00:00+00:00",
}
},
}
metadata_path.write_text(json.dumps(metadata, indent=2))
# Clear env to ensure we're testing file loading
monkeypatch.delenv("HINDSIGHT_API_LLM_PROVIDER", raising=False)
monkeypatch.delenv("HINDSIGHT_API_LLM_MODEL", raising=False)
# Test 1: Load default profile (no profile specified)
set_cli_profile_override(None)
load_config_file()
assert os.environ.get("HINDSIGHT_API_LLM_PROVIDER") == "openai"
assert os.environ.get("HINDSIGHT_API_LLM_MODEL") == "gpt-4o-mini"
# Clear env
monkeypatch.delenv("HINDSIGHT_API_LLM_PROVIDER")
monkeypatch.delenv("HINDSIGHT_API_LLM_MODEL")
# Test 2: Load named profile - should load ONLY that profile, not default
set_cli_profile_override(profile_name)
load_config_file()
# Should have loaded from the named profile
assert os.environ.get("HINDSIGHT_API_LLM_PROVIDER") == "groq", "Should load from named profile, not default"
assert os.environ.get("HINDSIGHT_API_LLM_MODEL") == "llama-3.1-70b", "Should load from named profile, not default"
def test_get_config_respects_profile(temp_home, monkeypatch):
"""Test that get_config() returns profile-specific values."""
import os
from hindsight_embed.cli import get_config, set_cli_profile_override
from hindsight_embed.profile_manager import ProfileManager
# Create default profile
default_config_dir = temp_home / ".hindsight"
default_config_dir.mkdir(parents=True, exist_ok=True)
(default_config_dir / "embed").write_text(
"HINDSIGHT_API_LLM_PROVIDER=openai\n"
"HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\n"
"HINDSIGHT_API_LLM_API_KEY=sk-default-key\n"
"HINDSIGHT_EMBED_BANK_ID=default-bank\n"
)
# Create named profile
profile_dir = temp_home / ".hindsight" / "profiles"
profile_dir.mkdir(parents=True, exist_ok=True)
profile_name = "production"
profile_env_path = profile_dir / f"{profile_name}.env"
profile_env_path.write_text(
"HINDSIGHT_API_LLM_PROVIDER=anthropic\n"
"HINDSIGHT_API_LLM_MODEL=claude-sonnet-4-20250514\n"
"HINDSIGHT_API_LLM_API_KEY=sk-ant-production\n"
"HINDSIGHT_EMBED_BANK_ID=production-bank\n"
)
# Create metadata
import json
metadata_path = profile_dir / "metadata.json"
metadata = {
"version": 1,
"profiles": {
profile_name: {
"port": 9900,
"created_at": "2024-01-01T00:00:00+00:00",
"last_used": "2024-01-01T00:00:00+00:00",
}
},
}
metadata_path.write_text(json.dumps(metadata, indent=2))
# Clear env
monkeypatch.delenv("HINDSIGHT_API_LLM_PROVIDER", raising=False)
monkeypatch.delenv("HINDSIGHT_API_LLM_MODEL", raising=False)
monkeypatch.delenv("HINDSIGHT_API_LLM_API_KEY", raising=False)
monkeypatch.delenv("HINDSIGHT_EMBED_BANK_ID", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
# Test with named profile
set_cli_profile_override(profile_name)
config = get_config()
# Should have loaded from production profile, NOT default
assert config["llm_provider"] == "anthropic", "Should use profile's provider"
assert config["llm_model"] == "claude-sonnet-4-20250514", "Should use profile's model"
assert config["llm_api_key"] == "sk-ant-production", "Should use profile's API key"
assert config["bank_id"] == "production-bank", "Should use profile's bank_id"
+18 -10
View File
@@ -8,8 +8,6 @@ from unittest.mock import Mock, patch
import pytest
from hindsight_embed.profile_manager import (
CONFIG_DIR,
PROFILES_DIR,
ProfileInfo,
ProfileManager,
ProfilePaths,
@@ -20,15 +18,25 @@ from hindsight_embed.profile_manager import (
@pytest.fixture
def temp_hindsight_dir(tmp_path, monkeypatch):
"""Create a temporary hindsight directory for tests."""
temp_config = tmp_path / ".hindsight"
"""Create a temporary hindsight directory for tests.
Uses HOME environment variable to make Path.home() return the temp directory.
This works with the dynamic path resolution in ProfileManager.
"""
# Clear any CLI profile override for test isolation
from hindsight_embed.cli import set_cli_profile_override
set_cli_profile_override(None)
# Clear HINDSIGHT_EMBED_PROFILE env var
monkeypatch.delenv("HINDSIGHT_EMBED_PROFILE", raising=False)
temp_home = tmp_path / "home"
temp_home.mkdir()
monkeypatch.setenv("HOME", str(temp_home))
temp_config = temp_home / ".hindsight"
temp_config.mkdir()
monkeypatch.setattr("hindsight_embed.profile_manager.CONFIG_DIR", temp_config)
monkeypatch.setattr("hindsight_embed.profile_manager.PROFILES_DIR", temp_config / "profiles")
monkeypatch.setattr(
"hindsight_embed.profile_manager.METADATA_FILE", temp_config / "profiles" / "metadata.json"
)
monkeypatch.setattr("hindsight_embed.profile_manager.ACTIVE_PROFILE_FILE", temp_config / "active_profile")
return temp_config