Files
2026-08-14 23:41:57 +08:00

204 lines
5.5 KiB
Go

package memory
import (
"path/filepath"
"testing"
)
func TestParsePathScopesAndTypes(t *testing.T) {
root := "/mem/root"
cases := []struct {
name string
path string
scope Scope
scopeID string
typ Type
}{
{
name: "global with type dir",
path: filepath.Join(root, "global", "user", "profile.md"),
scope: ScopeGlobal, scopeID: "", typ: TypeUser,
},
{
name: "global unknown type dir -> free",
path: filepath.Join(root, "global", "whatever", "x.md"),
scope: ScopeGlobal, scopeID: "", typ: TypeFree,
},
{
name: "global file directly under scope -> free",
path: filepath.Join(root, "global", "MEMORY.md"),
scope: ScopeGlobal, scopeID: "", typ: TypeFree,
},
{
name: "projects with type dir",
path: filepath.Join(root, "projects", "abc123", "checkpoint", "c1.md"),
scope: ScopeProjects, scopeID: "abc123", typ: TypeCheckpoint,
},
{
name: "projects file directly under id -> free",
path: filepath.Join(root, "projects", "abc123", "MEMORY.md"),
scope: ScopeProjects, scopeID: "abc123", typ: TypeFree,
},
{
name: "sessions with type dir",
path: filepath.Join(root, "sessions", "sess-1", "notes", "n.md"),
scope: ScopeSessions, scopeID: "sess-1", typ: TypeNotes,
},
{
name: "sessions progress type",
path: filepath.Join(root, "sessions", "sess-1", "progress", "p.md"),
scope: ScopeSessions, scopeID: "sess-1", typ: TypeProgress,
},
{
name: "nested file under type dir keeps type",
path: filepath.Join(root, "projects", "abc123", "reference", "sub", "r.md"),
scope: ScopeProjects, scopeID: "abc123", typ: TypeReference,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
loc, ok := parsePath(root, tc.path)
if !ok {
t.Fatalf("parsePath(%q) returned ok=false", tc.path)
}
if loc.Scope != tc.scope {
t.Errorf("scope = %q, want %q", loc.Scope, tc.scope)
}
if loc.ScopeID != tc.scopeID {
t.Errorf("scopeID = %q, want %q", loc.ScopeID, tc.scopeID)
}
if loc.Type != tc.typ {
t.Errorf("type = %q, want %q", loc.Type, tc.typ)
}
if loc.Path != filepath.Clean(tc.path) {
t.Errorf("path = %q, want %q", loc.Path, filepath.Clean(tc.path))
}
})
}
}
func TestParsePathOutsideLayout(t *testing.T) {
root := "/mem/root"
bad := []string{
"/other/place/x.md", // outside root
filepath.Join(root, "unknownscope", "x.md"), // not a layout scope
filepath.Join(root, "global"), // no file component
filepath.Join(root, "projects", "abc123"), // scope id dir, no file
filepath.Join(root, "global", "user", "x.txt"), // not markdown
}
for _, p := range bad {
if loc, ok := parsePath(root, p); ok {
t.Errorf("parsePath(%q) = %+v, ok=true; want ok=false", p, loc)
}
}
}
func TestParseCcPath(t *testing.T) {
base := "/home/u/.claude/projects"
loc, ok := parseCcPath(base, filepath.Join(base, "my-slug", "memory", "some", "note.md"))
if !ok {
t.Fatalf("parseCcPath returned ok=false")
}
if loc.Scope != ScopeCC {
t.Errorf("scope = %q, want %q", loc.Scope, ScopeCC)
}
if loc.ScopeID != "my-slug" {
t.Errorf("scopeID = %q, want %q", loc.ScopeID, "my-slug")
}
if loc.Type != TypeFree {
t.Errorf("type = %q, want %q", loc.Type, TypeFree)
}
bad := []string{
filepath.Join(base, "my-slug", "note.md"), // no memory segment
filepath.Join(base, "my-slug", "notmemory", "n.md"), // wrong segment
filepath.Join(base, "my-slug", "memory", "note.txt"), // not markdown
"/elsewhere/x/memory/n.md", // outside base
}
for _, p := range bad {
if loc, ok := parseCcPath(base, p); ok {
t.Errorf("parseCcPath(%q) = %+v, ok=true; want ok=false", p, loc)
}
}
}
func TestParseCcFrontmatterType(t *testing.T) {
cases := []struct {
name string
body string
want Type
}{
{
name: "nested metadata.type present",
body: "---\nmetadata:\n type: reference\n other: x\n---\nbody here\n",
want: TypeReference,
},
{
name: "top-level type present",
body: "---\ntype: feedback\n---\nbody\n",
want: TypeFeedback,
},
{
name: "metadata.type wins over top-level",
body: "---\ntype: free\nmetadata:\n type: project\n---\n",
want: TypeProject,
},
{
name: "absent frontmatter",
body: "no frontmatter here\n",
want: TypeFree,
},
{
name: "empty frontmatter",
body: "---\n---\nbody\n",
want: TypeFree,
},
{
name: "unknown type value",
body: "---\nmetadata:\n type: bogus\n---\n",
want: TypeFree,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := parseCcFrontmatterType(tc.body); got != tc.want {
t.Errorf("parseCcFrontmatterType = %q, want %q", got, tc.want)
}
})
}
}
func TestResolveProjectId(t *testing.T) {
const p = "/Users/dev/repo"
a := resolveProjectId(p)
b := resolveProjectId(p)
if a != b {
t.Errorf("resolveProjectId not stable: %q != %q", a, b)
}
if len(a) != 12 {
t.Errorf("resolveProjectId length = %d, want 12", len(a))
}
if resolveProjectId("/Users/dev/other") == a {
t.Errorf("resolveProjectId collided for distinct paths")
}
}
func TestAssertSafeComponent(t *testing.T) {
ok := []string{"user", "abc123", "sub/dir/file.md", "checkpoint"}
for _, s := range ok {
if err := assertSafeComponent(s); err != nil {
t.Errorf("assertSafeComponent(%q) = %v, want nil", s, err)
}
}
bad := []string{"", "..", "../etc", "a/../b", "/etc/passwd", "sub/../../x"}
for _, s := range bad {
if err := assertSafeComponent(s); err == nil {
t.Errorf("assertSafeComponent(%q) = nil, want error", s)
}
}
}