Viktor asked to remove the truncation from memory outputs after multiple agent sessions were misled by it: the 240-rune pretty preview cut memories mid-sentence, made sessions wrongly conclude no full-content read-back existed, and made a blind 'update --content' from the preview destroy the stored tail. The server never truncated — this was purely client-side display. recall/list now print each memory's full content (still one line per memory, newlines flattened). The per-turn recall hook pipes CLI output through verbatim, so injected memories are complete too. printMemories is split into a pure renderMemories for testability; truncatePreview and its UTF-8-boundary test are deleted (nothing is sliced anymore, so the invalid-UTF-8 class is gone by construction). v0.11.0 -> v0.12.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
func TestRenderMemoriesFullContent(t *testing.T) {
|
|
// The pretty view must NOT truncate content: the old 240-rune preview cut
|
|
// memories mid-sentence, misled agents into thinking no full-content
|
|
// read-back existed, and made blind `update --content` from the preview
|
|
// destroy the stored tail. Full passthrough also removes the mid-rune-cut
|
|
// invalid-UTF-8 class by construction — nothing is ever sliced.
|
|
long := strings.Repeat("я", 300) + strings.Repeat("a", 300)
|
|
raw, _ := json.Marshal(map[string]interface{}{"memories": []map[string]interface{}{
|
|
{"id": 7, "content": long, "category": "facts", "tags": "t1,t2", "importance": 0.7},
|
|
}})
|
|
got := renderMemories(raw, false)
|
|
if !strings.Contains(got, long) {
|
|
t.Fatalf("content was truncated: %q", got)
|
|
}
|
|
if strings.Contains(got, "…") {
|
|
t.Fatalf("ellipsis in output — truncation still active: %q", got)
|
|
}
|
|
if !utf8.ValidString(got) {
|
|
t.Fatalf("invalid UTF-8 in output: %q", got)
|
|
}
|
|
if !strings.Contains(got, "#7 [facts] (0.70) ") || !strings.Contains(got, "tags: t1,t2") {
|
|
t.Fatalf("line format broken: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRenderMemoriesFlattensNewlinesToOneLine(t *testing.T) {
|
|
// Consumers (the recall hook, terminal skims) rely on one memory per line;
|
|
// multi-line content is flattened, never split across lines.
|
|
raw, _ := json.Marshal(map[string]interface{}{"memories": []map[string]interface{}{
|
|
{"id": 1, "content": "line one\nline two\nline three", "category": "facts", "importance": 0.5},
|
|
}})
|
|
got := renderMemories(raw, false)
|
|
if !strings.Contains(got, "line one line two line three") {
|
|
t.Fatalf("newlines not flattened: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRenderMemoriesEdgeCases(t *testing.T) {
|
|
if got := renderMemories([]byte(`{"memories":[]}`), false); got != "(no memories)\n" {
|
|
t.Fatalf("empty list: %q", got)
|
|
}
|
|
// --json and unparseable responses pass through raw.
|
|
if got := renderMemories([]byte(`{"x":1}`), true); got != "{\"x\":1}\n" {
|
|
t.Fatalf("json passthrough: %q", got)
|
|
}
|
|
if got := renderMemories([]byte(`not json`), false); got != "not json\n" {
|
|
t.Fatalf("unparseable passthrough: %q", got)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|