mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-06-05 23:03:55 +00:00
4f4b4ac70f
- [x] Add language-specific chunkers with AST parsing (Go, Python, TypeScript) - [x] Implement chunking manager to dispatch files to appropriate chunkers - [x] Integrate code chunks into vector sync for semantic search - [x] Add tree-sitter dependency for Python/TypeScript parsing - [x] Reorder struct fields for consistency across codebase - [x] Rename error variables to follow Go conventions (err → unmarshalErr, etc.) - [x] Add code chunk metadata to vector documents (language, symbol name, line ranges) - [x] Update worker service to initialize chunking pipeline with all three languages
437 lines
11 KiB
Go
437 lines
11 KiB
Go
// Package config provides configuration management for claude-mnemonic.
|
|
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
// ConfigSuite is a test suite for config operations.
|
|
type ConfigSuite struct {
|
|
suite.Suite
|
|
tempDir string
|
|
origHomeDir string
|
|
}
|
|
|
|
func (s *ConfigSuite) SetupTest() {
|
|
var err error
|
|
s.tempDir, err = os.MkdirTemp("", "config-test-*")
|
|
s.Require().NoError(err)
|
|
|
|
// Save and override HOME
|
|
s.origHomeDir = os.Getenv("HOME")
|
|
os.Setenv("HOME", s.tempDir)
|
|
}
|
|
|
|
func (s *ConfigSuite) TearDownTest() {
|
|
os.Setenv("HOME", s.origHomeDir)
|
|
os.RemoveAll(s.tempDir)
|
|
}
|
|
|
|
func TestConfigSuite(t *testing.T) {
|
|
suite.Run(t, new(ConfigSuite))
|
|
}
|
|
|
|
// TestDefault tests default configuration values.
|
|
func (s *ConfigSuite) TestDefault() {
|
|
cfg := Default()
|
|
|
|
s.Equal(DefaultWorkerPort, cfg.WorkerPort)
|
|
s.Equal(DefaultModel, cfg.Model)
|
|
s.Equal(4, cfg.MaxConns)
|
|
s.Equal(100, cfg.ContextObservations)
|
|
s.Equal(25, cfg.ContextFullCount)
|
|
s.Equal(10, cfg.ContextSessionCount)
|
|
s.True(cfg.ContextShowReadTokens)
|
|
s.True(cfg.ContextShowWorkTokens)
|
|
s.Equal("narrative", cfg.ContextFullField)
|
|
s.True(cfg.ContextShowLastSummary)
|
|
s.Equal(DefaultObservationTypes, cfg.ContextObsTypes)
|
|
s.Equal(DefaultObservationConcepts, cfg.ContextObsConcepts)
|
|
}
|
|
|
|
// TestDataDir tests data directory path.
|
|
func (s *ConfigSuite) TestDataDir() {
|
|
dir := DataDir()
|
|
s.Contains(dir, ".claude-mnemonic")
|
|
}
|
|
|
|
// TestDBPath tests database path.
|
|
func (s *ConfigSuite) TestDBPath() {
|
|
path := DBPath()
|
|
s.Contains(path, "claude-mnemonic.db")
|
|
}
|
|
|
|
// TestSettingsPath tests settings file path.
|
|
func (s *ConfigSuite) TestSettingsPath() {
|
|
path := SettingsPath()
|
|
s.Contains(path, "settings.json")
|
|
}
|
|
|
|
// TestEnsureDataDir tests data directory creation.
|
|
func (s *ConfigSuite) TestEnsureDataDir() {
|
|
err := EnsureDataDir()
|
|
s.NoError(err)
|
|
|
|
dir := DataDir()
|
|
info, err := os.Stat(dir)
|
|
s.NoError(err)
|
|
s.True(info.IsDir())
|
|
}
|
|
|
|
// TestEnsureSettings tests settings file creation.
|
|
func (s *ConfigSuite) TestEnsureSettings() {
|
|
// First ensure data dir exists
|
|
err := EnsureDataDir()
|
|
s.NoError(err)
|
|
|
|
// Ensure settings creates default file
|
|
err = EnsureSettings()
|
|
s.NoError(err)
|
|
|
|
path := SettingsPath()
|
|
info, err := os.Stat(path)
|
|
s.NoError(err)
|
|
s.False(info.IsDir())
|
|
|
|
// Second call should not error (file exists)
|
|
err = EnsureSettings()
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestEnsureAll tests full initialization.
|
|
func (s *ConfigSuite) TestEnsureAll() {
|
|
err := EnsureAll()
|
|
s.NoError(err)
|
|
|
|
// Verify dir and settings exist
|
|
_, err = os.Stat(DataDir())
|
|
s.NoError(err)
|
|
_, err = os.Stat(SettingsPath())
|
|
s.NoError(err)
|
|
}
|
|
|
|
// TestLoad_TableDriven tests configuration loading with various scenarios.
|
|
func (s *ConfigSuite) TestLoad_TableDriven() {
|
|
tests := []struct {
|
|
name string
|
|
settingsJSON string
|
|
expectedModel string
|
|
expectedPort int
|
|
expectedObsObs int
|
|
}{
|
|
{
|
|
name: "no settings file",
|
|
settingsJSON: "",
|
|
expectedPort: DefaultWorkerPort,
|
|
expectedModel: DefaultModel,
|
|
expectedObsObs: 100,
|
|
},
|
|
{
|
|
name: "custom port",
|
|
settingsJSON: `{"CLAUDE_MNEMONIC_WORKER_PORT": 38888}`,
|
|
expectedPort: 38888,
|
|
expectedModel: DefaultModel,
|
|
expectedObsObs: 100,
|
|
},
|
|
{
|
|
name: "custom model",
|
|
settingsJSON: `{"CLAUDE_MNEMONIC_MODEL": "sonnet"}`,
|
|
expectedPort: DefaultWorkerPort,
|
|
expectedModel: "sonnet",
|
|
expectedObsObs: 100,
|
|
},
|
|
{
|
|
name: "custom observations",
|
|
settingsJSON: `{"CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS": 200}`,
|
|
expectedPort: DefaultWorkerPort,
|
|
expectedModel: DefaultModel,
|
|
expectedObsObs: 200,
|
|
},
|
|
{
|
|
name: "multiple settings",
|
|
settingsJSON: `{"CLAUDE_MNEMONIC_WORKER_PORT": 39999, "CLAUDE_MNEMONIC_MODEL": "opus", "CLAUDE_MNEMONIC_CONTEXT_OBSERVATIONS": 50}`,
|
|
expectedPort: 39999,
|
|
expectedModel: "opus",
|
|
expectedObsObs: 50,
|
|
},
|
|
{
|
|
name: "invalid JSON returns defaults",
|
|
settingsJSON: `{invalid}`,
|
|
expectedPort: DefaultWorkerPort,
|
|
expectedModel: DefaultModel,
|
|
expectedObsObs: 100,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
s.Run(tt.name, func() {
|
|
// Create fresh temp dir
|
|
tempDir, err := os.MkdirTemp("", "config-test-*")
|
|
s.Require().NoError(err)
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
os.Setenv("HOME", tempDir)
|
|
|
|
// Create data dir
|
|
err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750)
|
|
s.Require().NoError(err)
|
|
|
|
if tt.settingsJSON != "" {
|
|
writeErr := os.WriteFile(
|
|
filepath.Join(tempDir, ".claude-mnemonic", "settings.json"),
|
|
[]byte(tt.settingsJSON),
|
|
0600,
|
|
)
|
|
s.Require().NoError(writeErr)
|
|
}
|
|
|
|
cfg, err := Load()
|
|
s.NoError(err)
|
|
s.NotNil(cfg)
|
|
s.Equal(tt.expectedPort, cfg.WorkerPort)
|
|
s.Equal(tt.expectedModel, cfg.Model)
|
|
s.Equal(tt.expectedObsObs, cfg.ContextObservations)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGetWorkerPort_TableDriven tests worker port retrieval with various scenarios.
|
|
func TestGetWorkerPort_TableDriven(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
envValue string
|
|
wantPort int
|
|
setEnv bool
|
|
}{
|
|
{
|
|
name: "no env, use default",
|
|
envValue: "",
|
|
wantPort: DefaultWorkerPort,
|
|
setEnv: false,
|
|
},
|
|
{
|
|
name: "env set to valid port",
|
|
envValue: "38888",
|
|
wantPort: 38888,
|
|
setEnv: true,
|
|
},
|
|
{
|
|
name: "env set to invalid value",
|
|
envValue: "invalid",
|
|
wantPort: DefaultWorkerPort,
|
|
setEnv: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Save original env
|
|
origEnv := os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT")
|
|
defer os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", origEnv)
|
|
|
|
if tt.setEnv {
|
|
os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", tt.envValue)
|
|
} else {
|
|
os.Unsetenv("CLAUDE_MNEMONIC_WORKER_PORT")
|
|
}
|
|
|
|
// We can't easily test GetWorkerPort since it uses Get() which caches
|
|
// So we test the env parsing logic directly
|
|
if tt.setEnv && tt.envValue != "" {
|
|
if tt.wantPort != DefaultWorkerPort {
|
|
assert.Equal(t, tt.envValue, os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT"))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSplitTrim tests the splitTrim helper function.
|
|
func TestSplitTrim(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected []string
|
|
}{
|
|
{
|
|
name: "empty string",
|
|
input: "",
|
|
expected: []string{},
|
|
},
|
|
{
|
|
name: "single value",
|
|
input: "bugfix",
|
|
expected: []string{"bugfix"},
|
|
},
|
|
{
|
|
name: "multiple values",
|
|
input: "bugfix,feature,refactor",
|
|
expected: []string{"bugfix", "feature", "refactor"},
|
|
},
|
|
{
|
|
name: "values with spaces",
|
|
input: " bugfix , feature , refactor ",
|
|
expected: []string{"bugfix", "feature", "refactor"},
|
|
},
|
|
{
|
|
name: "empty values filtered",
|
|
input: "bugfix,,feature,,",
|
|
expected: []string{"bugfix", "feature"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := splitTrim(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDefaultObservationTypes tests default observation types.
|
|
func TestDefaultObservationTypes(t *testing.T) {
|
|
expected := []string{
|
|
"bugfix", "feature", "refactor", "change", "discovery", "decision",
|
|
}
|
|
assert.Equal(t, expected, DefaultObservationTypes)
|
|
}
|
|
|
|
// TestDefaultObservationConcepts tests default observation concepts.
|
|
func TestDefaultObservationConcepts(t *testing.T) {
|
|
expected := []string{
|
|
"how-it-works", "why-it-exists", "what-changed",
|
|
"problem-solution", "gotcha", "pattern", "trade-off",
|
|
}
|
|
assert.Equal(t, expected, DefaultObservationConcepts)
|
|
}
|
|
|
|
// TestCriticalConcepts tests critical concepts list.
|
|
func TestCriticalConcepts(t *testing.T) {
|
|
expected := []string{
|
|
"gotcha", "pattern", "problem-solution", "trade-off",
|
|
}
|
|
assert.Equal(t, expected, CriticalConcepts)
|
|
}
|
|
|
|
// TestLoad_ClaudeCodePath tests claude code path loading.
|
|
func TestLoad_ClaudeCodePath(t *testing.T) {
|
|
// Create temp dir
|
|
tempDir, err := os.MkdirTemp("", "config-test-*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
origHome := os.Getenv("HOME")
|
|
os.Setenv("HOME", tempDir)
|
|
defer os.Setenv("HOME", origHome)
|
|
|
|
// Create data dir and settings
|
|
err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750)
|
|
require.NoError(t, err)
|
|
|
|
settingsJSON := `{"CLAUDE_CODE_PATH": "/usr/local/bin/claude"}`
|
|
err = os.WriteFile(
|
|
filepath.Join(tempDir, ".claude-mnemonic", "settings.json"),
|
|
[]byte(settingsJSON),
|
|
0600,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/usr/local/bin/claude", cfg.ClaudeCodePath)
|
|
}
|
|
|
|
// TestGet tests the global config getter.
|
|
func TestGet(t *testing.T) {
|
|
// Save and restore HOME
|
|
origHome := os.Getenv("HOME")
|
|
tempDir, err := os.MkdirTemp("", "config-get-test-*")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
os.Setenv("HOME", origHome)
|
|
os.RemoveAll(tempDir)
|
|
}()
|
|
os.Setenv("HOME", tempDir)
|
|
|
|
// Create data dir
|
|
err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750)
|
|
require.NoError(t, err)
|
|
|
|
// Get() should return a valid config
|
|
cfg := Get()
|
|
require.NotNil(t, cfg)
|
|
assert.Greater(t, cfg.WorkerPort, 0)
|
|
assert.NotEmpty(t, cfg.Model)
|
|
}
|
|
|
|
// TestGetWorkerPort_WithEnv tests GetWorkerPort with environment variable.
|
|
func TestGetWorkerPort_WithEnv(t *testing.T) {
|
|
// Save original env
|
|
origEnv := os.Getenv("CLAUDE_MNEMONIC_WORKER_PORT")
|
|
defer os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", origEnv)
|
|
|
|
// Test with valid port in env
|
|
os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "45678")
|
|
port := GetWorkerPort()
|
|
assert.Equal(t, 45678, port)
|
|
|
|
// Test with invalid port (should fall back to config)
|
|
os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "not-a-number")
|
|
port = GetWorkerPort()
|
|
// Should return from Get().WorkerPort, which is default
|
|
assert.Greater(t, port, 0)
|
|
|
|
// Test with zero port (should fall back to config)
|
|
os.Setenv("CLAUDE_MNEMONIC_WORKER_PORT", "0")
|
|
port = GetWorkerPort()
|
|
// Zero is invalid, so should use default
|
|
assert.Greater(t, port, 0)
|
|
|
|
// Test with no env (should use config)
|
|
os.Unsetenv("CLAUDE_MNEMONIC_WORKER_PORT")
|
|
port = GetWorkerPort()
|
|
assert.Greater(t, port, 0)
|
|
}
|
|
|
|
// TestLoad_ContextSettings tests context-related settings loading.
|
|
func TestLoad_ContextSettings(t *testing.T) {
|
|
// Create temp dir
|
|
tempDir, err := os.MkdirTemp("", "config-test-*")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
origHome := os.Getenv("HOME")
|
|
os.Setenv("HOME", tempDir)
|
|
defer os.Setenv("HOME", origHome)
|
|
|
|
// Create data dir and settings
|
|
err = os.MkdirAll(filepath.Join(tempDir, ".claude-mnemonic"), 0750)
|
|
require.NoError(t, err)
|
|
|
|
settingsJSON := `{
|
|
"CLAUDE_MNEMONIC_CONTEXT_FULL_COUNT": 50,
|
|
"CLAUDE_MNEMONIC_CONTEXT_SESSION_COUNT": 20,
|
|
"CLAUDE_MNEMONIC_CONTEXT_OBS_TYPES": "bugfix,feature",
|
|
"CLAUDE_MNEMONIC_CONTEXT_OBS_CONCEPTS": "security,performance"
|
|
}`
|
|
err = os.WriteFile(
|
|
filepath.Join(tempDir, ".claude-mnemonic", "settings.json"),
|
|
[]byte(settingsJSON),
|
|
0600,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
cfg, err := Load()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 50, cfg.ContextFullCount)
|
|
assert.Equal(t, 20, cfg.ContextSessionCount)
|
|
assert.Equal(t, []string{"bugfix", "feature"}, cfg.ContextObsTypes)
|
|
assert.Equal(t, []string{"security", "performance"}, cfg.ContextObsConcepts)
|
|
}
|