Files
2026-07-13 13:00:08 +08:00

287 lines
7.4 KiB
Go

package memory
import (
"os"
"path/filepath"
"strings"
"testing"
)
// --- splitFrontmatter ---
func TestSplitFrontmatterNoFence(t *testing.T) {
fm, body := splitFrontmatter("just plain text\nno frontmatter")
if len(fm) != 0 {
t.Errorf("expected empty fm, got %v", fm)
}
if !strings.Contains(body, "just plain text") {
t.Errorf("body should contain original text: %q", body)
}
}
func TestSplitFrontmatterUnclosedFence(t *testing.T) {
input := "---\nname: test\ndescription: desc\n\nsome body without closing fence"
fm, body := splitFrontmatter(input)
// Unclosed fence: treat all as body.
if len(fm) != 0 {
t.Errorf("unclosed fence should return empty fm, got %v", fm)
}
if !strings.Contains(body, "---") {
t.Errorf("body should contain the original content: %q", body)
}
}
func TestSplitFrontmatterEmptyBody(t *testing.T) {
input := "---\nname: test\n---\n"
fm, body := splitFrontmatter(input)
if fm["name"] != "test" {
t.Errorf("name = %q", fm["name"])
}
if strings.TrimSpace(body) != "" {
t.Errorf("expected empty body, got %q", body)
}
}
func TestSplitFrontmatterNestedMetadata(t *testing.T) {
input := "---\nname: my-fact\ndescription: a desc\nmetadata:\n type: user\n---\n\nbody here"
fm, body := splitFrontmatter(input)
if fm["name"] != "my-fact" {
t.Errorf("name = %q", fm["name"])
}
if fm["description"] != "a desc" {
t.Errorf("description = %q", fm["description"])
}
// The nested " type: user" should flatten to fm["type"].
if fm["type"] != "user" {
t.Errorf("type = %q, expected flattened from metadata", fm["type"])
}
if !strings.Contains(body, "body here") {
t.Errorf("body = %q", body)
}
}
func TestSplitFrontmatterCRLF(t *testing.T) {
input := "---\r\nname: test\r\n---\r\nbody\r\n"
fm, body := splitFrontmatter(input)
if fm["name"] != "test" {
t.Errorf("name = %q", fm["name"])
}
if !strings.Contains(body, "body") {
t.Errorf("body = %q", body)
}
}
func TestSplitFrontmatterQuotedValues(t *testing.T) {
input := "---\nname: test\ndescription: \"quoted desc\"\n---\n"
fm, _ := splitFrontmatter(input)
if fm["description"] != "quoted desc" {
t.Errorf("description should be unquoted: %q", fm["description"])
}
}
// --- slug ---
func TestSlug(t *testing.T) {
cases := []struct {
input string
want string
}{
{"Prefers Tabs", "prefers-tabs"},
{" spaces ", "spaces"},
{"CamelCase", "camelcase"},
{"with/slash", "with-slash"},
{"", ""},
{"---", ""},
{"hello_world", "hello-world"},
{"中文标题", "中文标题"},
{"HÉLLO", "héllo"},
{"日本語123", "日本語123"},
}
for _, c := range cases {
got := slug(c.input)
if got != c.want {
t.Errorf("slug(%q) = %q, want %q", c.input, got, c.want)
}
}
}
func TestStoreSaveUnicodeAndRejectsEmptySlug(t *testing.T) {
s := Store{Dir: t.TempDir()}
path, err := s.Save(Memory{Name: "中文标题", Description: "d", Type: TypeProject, Body: "body"})
if err != nil {
t.Fatal(err)
}
if filepath.Base(path) != "中文标题.md" {
t.Fatalf("unicode name path = %q", path)
}
if _, err := s.Save(Memory{Name: "---", Description: "d", Type: TypeProject, Body: "body"}); err == nil {
t.Fatal("punctuation-only name should be rejected")
}
}
// --- oneLine ---
func TestOneLine(t *testing.T) {
cases := []struct {
input string
want string
}{
{"hello world", "hello world"},
{" multiple spaces ", "multiple spaces"},
{"tabs\there", "tabs here"},
{"\n\nnewlines\n\n", "newlines"},
{"", ""},
}
for _, c := range cases {
got := oneLine(c.input)
if got != c.want {
t.Errorf("oneLine(%q) = %q, want %q", c.input, got, c.want)
}
}
}
// --- render ---
func TestRenderRoundTrip(t *testing.T) {
m := Memory{
Name: "test-fact",
Description: "A test fact",
Type: TypeUser,
Body: "The body of the fact.",
}
rendered := render(m, "test-fact")
fm, body := splitFrontmatter(rendered)
if fm["name"] != "test-fact" {
t.Errorf("name = %q", fm["name"])
}
if fm["description"] != "A test fact" {
t.Errorf("description = %q", fm["description"])
}
if fm["type"] != "user" {
t.Errorf("type = %q", fm["type"])
}
if !strings.Contains(body, "The body of the fact.") {
t.Errorf("body = %q", body)
}
}
func TestRenderNormalizesType(t *testing.T) {
m := Memory{Name: "x", Description: "d", Type: Type("unknown"), Body: "b"}
rendered := render(m, "x")
fm, _ := splitFrontmatter(rendered)
if fm["type"] != "project" {
t.Errorf("unknown type should normalize to project, got %q", fm["type"])
}
}
// --- loadMemory ---
func TestLoadMemoryNoFrontmatter(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "no-fm.md")
os.WriteFile(f, []byte("just a body\nno frontmatter"), 0o644)
m, ok := loadMemory(f)
if !ok {
t.Fatal("loadMemory should succeed for files without frontmatter")
}
// Name should be derived from filename.
if m.Name != "no-fm" {
t.Errorf("name = %q, want no-fm", m.Name)
}
if !strings.Contains(m.Body, "just a body") {
t.Errorf("body = %q", m.Body)
}
}
func TestLoadMemoryMissingFile(t *testing.T) {
_, ok := loadMemory("/nonexistent/path.md")
if ok {
t.Error("loadMemory should return false for missing file")
}
}
func TestLoadMemoryEmptyFile(t *testing.T) {
dir := t.TempDir()
f := filepath.Join(dir, "empty.md")
os.WriteFile(f, nil, 0o644)
m, ok := loadMemory(f)
if !ok {
t.Fatal("loadMemory should succeed for empty files")
}
if m.Name != "empty" {
t.Errorf("name = %q", m.Name)
}
}
// --- Store.List edge cases ---
func TestListSkipsNonMdFiles(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "fact.md"), []byte("---\nname: fact\n---\nbody"), 0o644)
os.WriteFile(filepath.Join(dir, "notes.txt"), []byte("not a memory"), 0o644)
os.WriteFile(filepath.Join(dir, "MEMORY.md"), []byte("# Memory\n"), 0o644)
s := Store{Dir: dir}
list := s.List()
if len(list) != 1 {
t.Fatalf("want 1 memory, got %d", len(list))
}
if list[0].Name != "fact" {
t.Errorf("name = %q", list[0].Name)
}
}
func TestListEmptyDir(t *testing.T) {
s := Store{Dir: t.TempDir()}
if list := s.List(); len(list) != 0 {
t.Errorf("empty dir should return empty list, got %d", len(list))
}
}
func TestListSortedByName(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"zebra", "alpha", "middle"} {
os.WriteFile(filepath.Join(dir, name+".md"), []byte("---\nname: "+name+"\n---\nbody"), 0o644)
}
s := Store{Dir: dir}
list := s.List()
if len(list) != 3 {
t.Fatalf("want 3, got %d", len(list))
}
if list[0].Name != "alpha" || list[1].Name != "middle" || list[2].Name != "zebra" {
t.Errorf("not sorted: %v %v %v", list[0].Name, list[1].Name, list[2].Name)
}
}
// --- Store.Save edge cases ---
func TestSaveEmptyName(t *testing.T) {
s := Store{Dir: t.TempDir()}
_, err := s.Save(Memory{Name: "", Description: "d", Body: "b"})
if err == nil {
t.Fatal("expected error for empty name")
}
}
func TestSaveCreatesDir(t *testing.T) {
dir := t.TempDir()
s := Store{Dir: filepath.Join(dir, "deep", "nested", "memory")}
_, err := s.Save(Memory{Name: "test", Description: "d", Body: "b"})
if err != nil {
t.Fatalf("Save should create dirs: %v", err)
}
if _, err := os.Stat(filepath.Join(s.Dir, "test.md")); err != nil {
t.Fatal("memory file should exist")
}
}
// --- Store.Path ---
func TestStorePath(t *testing.T) {
dir := filepath.Join(t.TempDir(), "memory")
s := Store{Dir: dir}
got := s.Path("My Fact")
if want := filepath.Join(dir, "my-fact.md"); got != want {
t.Errorf("Path = %q, want %q", got, want)
}
}