287 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|