homelab: add memory verb-group (v0.3.0) — direct claude-memory HTTP client
Lets agents search/navigate memory via the CLI, as the first step toward
deprecating the memory MCP. claude-memory is a FastAPI service (the MCP is just
one frontend); homelab memory is a thin Bearer-auth HTTP client over the same
API, using the env the hooks already set (CLAUDE_MEMORY_API_URL/KEY). It works
even when the MCP frontend is down — the recurring disconnect that took the MCP
offline for this whole session.
Verbs: recall (server-side semantic search), list, categories, tags, stats,
secret (read); store, update, delete (write). Validated against the live API
including a store→recall→delete round-trip — full data-plane parity with the MCP.
The deprecation itself (rewiring the per-prompt auto-recall + auto-learn hooks to
the CLI, then uninstalling the MCP) is a deliberate follow-up, sequenced after
the CLI is proven in the hooks — see docs/adr/0008.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 05:56:25 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"os"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-06-28 09:40:51 +00:00
|
|
|
"unicode/utf8"
|
homelab: add memory verb-group (v0.3.0) — direct claude-memory HTTP client
Lets agents search/navigate memory via the CLI, as the first step toward
deprecating the memory MCP. claude-memory is a FastAPI service (the MCP is just
one frontend); homelab memory is a thin Bearer-auth HTTP client over the same
API, using the env the hooks already set (CLAUDE_MEMORY_API_URL/KEY). It works
even when the MCP frontend is down — the recurring disconnect that took the MCP
offline for this whole session.
Verbs: recall (server-side semantic search), list, categories, tags, stats,
secret (read); store, update, delete (write). Validated against the live API
including a store→recall→delete round-trip — full data-plane parity with the MCP.
The deprecation itself (rewiring the per-prompt auto-recall + auto-learn hooks to
the CLI, then uninstalling the MCP) is a deliberate follow-up, sequenced after
the CLI is proven in the hooks — see docs/adr/0008.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 05:56:25 +00:00
|
|
|
)
|
|
|
|
|
|
2026-06-28 09:40:51 +00:00
|
|
|
func TestTruncatePreviewKeepsValidUTF8(t *testing.T) {
|
|
|
|
|
// Byte-slicing a long Cyrillic string at 240 splits a 2-byte rune and emits
|
|
|
|
|
// invalid UTF-8 — the bug that crashed the recall hook. truncatePreview must
|
|
|
|
|
// cut on a rune boundary and always stay valid UTF-8.
|
|
|
|
|
long := strings.Repeat("я", 300) // 300 runes / 600 bytes
|
|
|
|
|
got := truncatePreview(long, 240)
|
|
|
|
|
if !utf8.ValidString(got) {
|
|
|
|
|
t.Fatalf("truncatePreview produced invalid UTF-8: %q", got)
|
|
|
|
|
}
|
|
|
|
|
if r := []rune(got); len(r) != 241 || string(r[:240]) != strings.Repeat("я", 240) || r[240] != '…' {
|
|
|
|
|
t.Fatalf("truncatePreview = %d runes, want 240 Cyrillic + ellipsis", len(r))
|
|
|
|
|
}
|
|
|
|
|
// Short multibyte strings pass through untouched (no ellipsis).
|
|
|
|
|
if got := truncatePreview("кратко", 240); got != "кратко" {
|
|
|
|
|
t.Fatalf("short string altered: %q", got)
|
|
|
|
|
}
|
|
|
|
|
// ASCII boundary still works.
|
|
|
|
|
if got := truncatePreview(strings.Repeat("a", 500), 240); got != strings.Repeat("a", 240)+"…" {
|
|
|
|
|
t.Fatalf("ascii truncation wrong: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
homelab: add memory verb-group (v0.3.0) — direct claude-memory HTTP client
Lets agents search/navigate memory via the CLI, as the first step toward
deprecating the memory MCP. claude-memory is a FastAPI service (the MCP is just
one frontend); homelab memory is a thin Bearer-auth HTTP client over the same
API, using the env the hooks already set (CLAUDE_MEMORY_API_URL/KEY). It works
even when the MCP frontend is down — the recurring disconnect that took the MCP
offline for this whole session.
Verbs: recall (server-side semantic search), list, categories, tags, stats,
secret (read); store, update, delete (write). Validated against the live API
including a store→recall→delete round-trip — full data-plane parity with the MCP.
The deprecation itself (rewiring the per-prompt auto-recall + auto-learn hooks to
the CLI, then uninstalling the MCP) is a deliberate follow-up, sequenced after
the CLI is proven in the hooks — see docs/adr/0008.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 05:56:25 +00:00
|
|
|
func TestResolveMemoryBase(t *testing.T) {
|
|
|
|
|
old1, old2 := os.Getenv("CLAUDE_MEMORY_API_URL"), os.Getenv("MEMORY_API_URL")
|
|
|
|
|
defer func() { os.Setenv("CLAUDE_MEMORY_API_URL", old1); os.Setenv("MEMORY_API_URL", old2) }()
|
|
|
|
|
|
|
|
|
|
os.Unsetenv("CLAUDE_MEMORY_API_URL")
|
|
|
|
|
os.Unsetenv("MEMORY_API_URL")
|
|
|
|
|
if got := resolveMemoryBase(); got != defaultMemoryURL {
|
|
|
|
|
t.Errorf("resolveMemoryBase() = %q, want default %q", got, defaultMemoryURL)
|
|
|
|
|
}
|
|
|
|
|
os.Setenv("CLAUDE_MEMORY_API_URL", "https://m.example/") // trailing slash trimmed
|
|
|
|
|
if got := resolveMemoryBase(); got != "https://m.example" {
|
|
|
|
|
t.Errorf("resolveMemoryBase() = %q, want https://m.example", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMemStoreReqAlwaysSendsImportance(t *testing.T) {
|
|
|
|
|
b, _ := json.Marshal(memStoreReq{Content: "x", Category: "facts", Importance: 0.5})
|
|
|
|
|
s := string(b)
|
|
|
|
|
if !strings.Contains(s, `"content":"x"`) || !strings.Contains(s, `"importance":0.5`) {
|
|
|
|
|
t.Fatalf("memStoreReq JSON missing fields: %s", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMemUpdateReqOmitsUnsetFields(t *testing.T) {
|
|
|
|
|
tags := "a,b"
|
|
|
|
|
b, _ := json.Marshal(memUpdateReq{Tags: &tags})
|
|
|
|
|
s := string(b)
|
|
|
|
|
if strings.Contains(s, "content") || strings.Contains(s, "importance") {
|
|
|
|
|
t.Fatalf("unset update fields must be omitted: %s", s)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(s, `"tags":"a,b"`) {
|
|
|
|
|
t.Fatalf("set field missing: %s", s)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMemRecallReqOmitsEmptyOptionals(t *testing.T) {
|
|
|
|
|
b, _ := json.Marshal(memRecallReq{Context: "hi"})
|
|
|
|
|
s := string(b)
|
|
|
|
|
if strings.Contains(s, "expanded_query") || strings.Contains(s, "category") || strings.Contains(s, "limit") {
|
|
|
|
|
t.Fatalf("empty optionals must be omitted: %s", s)
|
|
|
|
|
}
|
|
|
|
|
}
|