package memory import ( "context" "os" "path/filepath" "strings" "testing" ) // ── 版本常量 ───────────────────────────────────────────────────────────────── func TestFrontmatterVersionConstants(t *testing.T) { if frontmatterCurrentVersion < 1 { t.Errorf("frontmatterCurrentVersion = %d, want >= 1", frontmatterCurrentVersion) } if frontmatterMaxSupportedVersion != frontmatterCurrentVersion { t.Errorf("max(%d) != current(%d)", frontmatterMaxSupportedVersion, frontmatterCurrentVersion) } } // ── ParseFrontmatter 解析 version 字段 ──────────────────────────────────────── func TestParseFrontmatter_WithVersion(t *testing.T) { content := "---\nname: test\ndescription: desc\ntype: user\nversion: 1\n---\nbody" fm, body, ok := ParseFrontmatter(content) if !ok { t.Fatal("ParseFrontmatter returned ok=false") } if fm.Version != 1 { t.Errorf("Version = %d, want 1", fm.Version) } if body != "body" { t.Errorf("body = %q, want \"body\"", body) } } func TestParseFrontmatter_NoVersion_ReturnsZero(t *testing.T) { // 旧格式文件:无 version 字段 content := "---\nname: old\ntype: project\n---\ncontent" fm, _, ok := ParseFrontmatter(content) if !ok { t.Fatal("ParseFrontmatter returned ok=false for old format") } // 无 version 字段 → 零值 if fm.Version != 0 { t.Errorf("Version = %d, want 0 (missing = old format)", fm.Version) } } func TestParseFrontmatter_InvalidVersion_ReturnsZero(t *testing.T) { content := "---\nname: x\ntype: user\nversion: notanumber\n---\n" fm, _, _ := ParseFrontmatter(content) if fm.Version != 0 { t.Errorf("invalid version should parse as 0, got %d", fm.Version) } } // ── FormatFrontmatter 写入 version 字段 ─────────────────────────────────────── func TestFormatFrontmatter_WritesVersion(t *testing.T) { fm := Frontmatter{Name: "n", Type: TypeUser, Version: 1} out := FormatFrontmatter(fm, "body") if !strings.Contains(out, "version: 1") { t.Errorf("FormatFrontmatter should write 'version: 1', got:\n%s", out) } } func TestFormatFrontmatter_ZeroVersion_UsesCurrentVersion(t *testing.T) { fm := Frontmatter{Name: "n", Type: TypeUser, Version: 0} out := FormatFrontmatter(fm, "") expected := "version: " + strings.TrimSpace(strings.Split(out, "\n")[3]) // version: 1 が含まれているはず if !strings.Contains(out, "version: 1") { t.Errorf("zero Version should default to current version (1), got:\n%s", out) } _ = expected } // ── Save() 写入 version 字段 ────────────────────────────────────────────────── func TestSave_WritesVersionInFrontmatter(t *testing.T) { dir := t.TempDir() store := NewFileStoreWithBaseDir(dir) ctx := context.Background() entry := &Entry{Name: "v_test", Type: TypeFeedback, Content: "data"} if err := store.Save(ctx, entry); err != nil { t.Fatalf("Save: %v", err) } // 读回文件内容验证 version 字段 files, _ := os.ReadDir(dir) var mdPath string for _, f := range files { if strings.HasSuffix(f.Name(), ".md") && f.Name() != "MEMORY.md" { mdPath = filepath.Join(dir, f.Name()) break } } if mdPath == "" { t.Fatal("no .md file written") } data, _ := os.ReadFile(mdPath) if !strings.Contains(string(data), "version: 1") { t.Errorf("saved file should contain 'version: 1', got:\n%s", data) } } // ── ScanMemoryDir MaxSupportedVersion 保护 ───────────────────────────────────── func TestScanMemoryDir_FutureVersion_SkipsFile(t *testing.T) { dir := t.TempDir() // 写一个版本号超出支持范围的记忆文件 futureVersion := frontmatterMaxSupportedVersion + 1 content := "---\nname: future\ntype: user\nversion: " + strings.TrimSpace(strings.Join(strings.Fields(strings.Repeat("x", futureVersion)), "")) + "\n---\nbody" // 正确写入数字版本 content = "---\nname: future\ntype: user\nversion: 999\n---\nbody" if err := os.WriteFile(filepath.Join(dir, "future.md"), []byte(content), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } // 同时写一个正常版本文件 normalContent := "---\nname: normal\ntype: user\nversion: 1\n---\nbody" if err := os.WriteFile(filepath.Join(dir, "normal.md"), []byte(normalContent), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } headers, err := ScanMemoryDir(dir) if err != nil { t.Fatalf("ScanMemoryDir: %v", err) } // 只有 normal 文件应该出现 if len(headers) != 1 { t.Errorf("expected 1 header (future skipped), got %d", len(headers)) } if len(headers) > 0 && headers[0].Frontmatter.Name != "normal" { t.Errorf("expected 'normal', got %q", headers[0].Frontmatter.Name) } } func TestScanMemoryDir_OldFile_NormalizesToV1(t *testing.T) { dir := t.TempDir() // 写旧格式文件(无 version 字段) oldContent := "---\nname: oldmem\ntype: project\n---\nsome content" if err := os.WriteFile(filepath.Join(dir, "old.md"), []byte(oldContent), 0644); err != nil { t.Fatalf("WriteFile: %v", err) } headers, err := ScanMemoryDir(dir) if err != nil { t.Fatalf("ScanMemoryDir: %v", err) } if len(headers) != 1 { t.Fatalf("expected 1 header, got %d", len(headers)) } // 旧文件规范化为 v1 if headers[0].Frontmatter.Version != 1 { t.Errorf("old file Version = %d, want 1 (normalized)", headers[0].Frontmatter.Version) } } // ── migrateFrontmatter 空表 no-op ───────────────────────────────────────────── func TestMigrateFrontmatter_EmptyTable_NoOp(t *testing.T) { fm := &Frontmatter{Version: frontmatterCurrentVersion, Name: "x"} if err := migrateFrontmatter(fm); err != nil { t.Errorf("migrateFrontmatter no-op: %v", err) } if fm.Version != frontmatterCurrentVersion { t.Errorf("Version changed: %d", fm.Version) } } func TestMigrateFrontmatter_MissingFunc_ReturnsError(t *testing.T) { // Version == 0 < frontmatterCurrentVersion(1),但 frontmatterMigrations[0] 不存在 fm := &Frontmatter{Version: 0} err := migrateFrontmatter(fm) if err == nil { t.Fatal("should error when migration func is missing") } if !strings.Contains(err.Error(), "no migration registered") { t.Errorf("error should mention 'no migration registered', got: %v", err) } }