Add BlobStore (ref counting), FileStore (soft delete + pagination), FolderStore (materialized path), UploadStore (idempotent upsert), and update AutoMigrate. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
246 lines
6.1 KiB
Go
246 lines
6.1 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"gcy_hpc_server/internal/model"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupFileTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(&model.File{}, &model.FileBlob{}); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestFileStore_CreateAndGetByID(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
file := &model.File{
|
|
Name: "test.bin",
|
|
BlobSHA256: "abc123",
|
|
}
|
|
if err := store.Create(ctx, file); err != nil {
|
|
t.Fatalf("Create() error = %v", err)
|
|
}
|
|
if file.ID == 0 {
|
|
t.Fatal("Create() did not set ID")
|
|
}
|
|
|
|
got, err := store.GetByID(ctx, file.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetByID() error = %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("GetByID() returned nil")
|
|
}
|
|
if got.Name != "test.bin" {
|
|
t.Errorf("Name = %q, want %q", got.Name, "test.bin")
|
|
}
|
|
if got.BlobSHA256 != "abc123" {
|
|
t.Errorf("BlobSHA256 = %q, want %q", got.BlobSHA256, "abc123")
|
|
}
|
|
}
|
|
|
|
func TestFileStore_GetByID_NotFound(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
got, err := store.GetByID(ctx, 999)
|
|
if err != nil {
|
|
t.Fatalf("GetByID() error = %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Error("GetByID() should return nil for not found")
|
|
}
|
|
}
|
|
|
|
func TestFileStore_ListByFolder(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
folderID := int64(1)
|
|
store.Create(ctx, &model.File{Name: "f1.bin", BlobSHA256: "a1", FolderID: &folderID})
|
|
store.Create(ctx, &model.File{Name: "f2.bin", BlobSHA256: "a2", FolderID: &folderID})
|
|
store.Create(ctx, &model.File{Name: "root.bin", BlobSHA256: "a3"}) // root (folder_id=nil)
|
|
|
|
files, total, err := store.List(ctx, &folderID, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("List() error = %v", err)
|
|
}
|
|
if total != 2 {
|
|
t.Errorf("total = %d, want 2", total)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Errorf("len(files) = %d, want 2", len(files))
|
|
}
|
|
}
|
|
|
|
func TestFileStore_ListRootFolder(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
store.Create(ctx, &model.File{Name: "root.bin", BlobSHA256: "a1"})
|
|
folderID := int64(1)
|
|
store.Create(ctx, &model.File{Name: "sub.bin", BlobSHA256: "a2", FolderID: &folderID})
|
|
|
|
files, total, err := store.List(ctx, nil, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("List() error = %v", err)
|
|
}
|
|
if total != 1 {
|
|
t.Errorf("total = %d, want 1", total)
|
|
}
|
|
if len(files) != 1 {
|
|
t.Errorf("len(files) = %d, want 1", len(files))
|
|
}
|
|
if files[0].Name != "root.bin" {
|
|
t.Errorf("files[0].Name = %q, want %q", files[0].Name, "root.bin")
|
|
}
|
|
}
|
|
|
|
func TestFileStore_Pagination(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 25; i++ {
|
|
store.Create(ctx, &model.File{Name: "file.bin", BlobSHA256: "hash"})
|
|
}
|
|
|
|
files, total, err := store.List(ctx, nil, 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("List() error = %v", err)
|
|
}
|
|
if total != 25 {
|
|
t.Errorf("total = %d, want 25", total)
|
|
}
|
|
if len(files) != 10 {
|
|
t.Errorf("page 1 len = %d, want 10", len(files))
|
|
}
|
|
|
|
files, _, _ = store.List(ctx, nil, 3, 10)
|
|
if len(files) != 5 {
|
|
t.Errorf("page 3 len = %d, want 5", len(files))
|
|
}
|
|
}
|
|
|
|
func TestFileStore_Search(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
store.Create(ctx, &model.File{Name: "experiment_results.csv", BlobSHA256: "a1"})
|
|
store.Create(ctx, &model.File{Name: "training_log.txt", BlobSHA256: "a2"})
|
|
store.Create(ctx, &model.File{Name: "model_weights.bin", BlobSHA256: "a3"})
|
|
|
|
files, total, err := store.Search(ctx, "results", 1, 10)
|
|
if err != nil {
|
|
t.Fatalf("Search() error = %v", err)
|
|
}
|
|
if total != 1 {
|
|
t.Errorf("total = %d, want 1", total)
|
|
}
|
|
if len(files) != 1 || files[0].Name != "experiment_results.csv" {
|
|
t.Errorf("expected experiment_results.csv, got %v", files)
|
|
}
|
|
}
|
|
|
|
func TestFileStore_Delete_SoftDelete(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
file := &model.File{Name: "deleteme.bin", BlobSHA256: "abc"}
|
|
store.Create(ctx, file)
|
|
|
|
if err := store.Delete(ctx, file.ID); err != nil {
|
|
t.Fatalf("Delete() error = %v", err)
|
|
}
|
|
|
|
// GetByID should return nil (soft deleted)
|
|
got, err := store.GetByID(ctx, file.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetByID() error = %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Error("GetByID() should return nil after soft delete")
|
|
}
|
|
|
|
// List should not include soft deleted
|
|
_, total, _ := store.List(ctx, nil, 1, 10)
|
|
if total != 0 {
|
|
t.Errorf("total after delete = %d, want 0", total)
|
|
}
|
|
}
|
|
|
|
func TestFileStore_CountByBlobSHA256(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
store.Create(ctx, &model.File{Name: "a.bin", BlobSHA256: "shared_hash"})
|
|
store.Create(ctx, &model.File{Name: "b.bin", BlobSHA256: "shared_hash"})
|
|
store.Create(ctx, &model.File{Name: "c.bin", BlobSHA256: "shared_hash"})
|
|
|
|
count, err := store.CountByBlobSHA256(ctx, "shared_hash")
|
|
if err != nil {
|
|
t.Fatalf("CountByBlobSHA256() error = %v", err)
|
|
}
|
|
if count != 3 {
|
|
t.Errorf("count = %d, want 3", count)
|
|
}
|
|
|
|
// Soft delete one
|
|
store.Delete(ctx, 1)
|
|
count, _ = store.CountByBlobSHA256(ctx, "shared_hash")
|
|
if count != 2 {
|
|
t.Errorf("count after soft delete = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
func TestFileStore_GetBlobSHA256ByID(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
file := &model.File{Name: "test.bin", BlobSHA256: "my_hash"}
|
|
store.Create(ctx, file)
|
|
|
|
sha256, err := store.GetBlobSHA256ByID(ctx, file.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetBlobSHA256ByID() error = %v", err)
|
|
}
|
|
if sha256 != "my_hash" {
|
|
t.Errorf("sha256 = %q, want %q", sha256, "my_hash")
|
|
}
|
|
}
|
|
|
|
func TestFileStore_GetBlobSHA256ByID_NotFound(t *testing.T) {
|
|
db := setupFileTestDB(t)
|
|
store := NewFileStore(db)
|
|
ctx := context.Background()
|
|
|
|
sha256, err := store.GetBlobSHA256ByID(ctx, 999)
|
|
if err != nil {
|
|
t.Fatalf("GetBlobSHA256ByID() error = %v", err)
|
|
}
|
|
if sha256 != "" {
|
|
t.Errorf("sha256 = %q, want empty for not found", sha256)
|
|
}
|
|
}
|