- Introduce new Tai SDK tests in the GitHub workflows, requiring a Tai container with Docker socket mount for execution. - Update the Makefile to include a dedicated target for running Tai SDK tests, enhancing test coverage for the Tai integration. - Modify the Go module dependencies to include the pierrec/lz4 package, ensuring compatibility with the new tests. - Adjust test folder selection logic in the Makefile to exclude additional directories, streamlining the testing process.
819 lines
22 KiB
Go
819 lines
22 KiB
Go
package volume
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
func taiTestGRPC() string {
|
|
if addr := os.Getenv("TAI_TEST_GRPC"); addr != "" {
|
|
return addr
|
|
}
|
|
return "127.0.0.1:9100"
|
|
}
|
|
|
|
func TestLocalVolume(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "test-session"
|
|
|
|
t.Run("WriteFile and ReadFile", func(t *testing.T) {
|
|
data := []byte("hello world")
|
|
if err := vol.WriteFile(ctx, sid, "greeting.txt", data, 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
got, mode, err := vol.ReadFile(ctx, sid, "greeting.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(got) != "hello world" {
|
|
t.Errorf("got %q, want %q", got, "hello world")
|
|
}
|
|
if mode&0o644 != 0o644 {
|
|
t.Errorf("mode %v does not contain 0644", mode)
|
|
}
|
|
})
|
|
|
|
t.Run("Stat", func(t *testing.T) {
|
|
info, err := vol.Stat(ctx, sid, "greeting.txt")
|
|
if err != nil {
|
|
t.Fatalf("Stat: %v", err)
|
|
}
|
|
if info.Size != 11 {
|
|
t.Errorf("size = %d, want 11", info.Size)
|
|
}
|
|
if info.IsDir {
|
|
t.Error("expected file, got dir")
|
|
}
|
|
})
|
|
|
|
t.Run("MkdirAll and ListDir", func(t *testing.T) {
|
|
if err := vol.MkdirAll(ctx, sid, "subdir/nested"); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
_ = vol.WriteFile(ctx, sid, "subdir/nested/file.txt", []byte("x"), 0o644)
|
|
entries, err := vol.ListDir(ctx, sid, "subdir/nested")
|
|
if err != nil {
|
|
t.Fatalf("ListDir: %v", err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("got %d entries, want 1", len(entries))
|
|
}
|
|
if entries[0].Path != "file.txt" {
|
|
t.Errorf("entry name = %q, want %q", entries[0].Path, "file.txt")
|
|
}
|
|
})
|
|
|
|
t.Run("Rename", func(t *testing.T) {
|
|
if err := vol.Rename(ctx, sid, "greeting.txt", "hello.txt"); err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
_, _, err := vol.ReadFile(ctx, sid, "hello.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile after rename: %v", err)
|
|
}
|
|
_, _, err = vol.ReadFile(ctx, sid, "greeting.txt")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Remove", func(t *testing.T) {
|
|
if err := vol.Remove(ctx, sid, "hello.txt", false); err != nil {
|
|
t.Fatalf("Remove: %v", err)
|
|
}
|
|
_, err := vol.Stat(ctx, sid, "hello.txt")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Remove recursive", func(t *testing.T) {
|
|
if err := vol.Remove(ctx, sid, "subdir", true); err != nil {
|
|
t.Fatalf("RemoveAll: %v", err)
|
|
}
|
|
_, err := vol.Stat(ctx, sid, "subdir")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLocalSyncPush(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "sync-test"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0o644)
|
|
_ = os.MkdirAll(filepath.Join(srcDir, "sub"), 0o755)
|
|
_ = os.WriteFile(filepath.Join(srcDir, "sub", "b.txt"), []byte("bbb"), 0o644)
|
|
|
|
result, err := vol.SyncPush(ctx, sid, srcDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 2 {
|
|
t.Errorf("synced = %d, want 2", result.FilesSynced)
|
|
}
|
|
|
|
// Verify files exist in dataDir
|
|
data, err := os.ReadFile(filepath.Join(dataDir, sid, "a.txt"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(data) != "aaa" {
|
|
t.Errorf("content = %q, want %q", data, "aaa")
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPull(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "pull-test"
|
|
|
|
// Create source in dataDir
|
|
sessionDir := filepath.Join(dataDir, sid)
|
|
_ = os.MkdirAll(sessionDir, 0o755)
|
|
_ = os.WriteFile(filepath.Join(sessionDir, "c.txt"), []byte("ccc"), 0o644)
|
|
|
|
dstDir := t.TempDir()
|
|
result, err := vol.SyncPull(ctx, sid, dstDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1", result.FilesSynced)
|
|
}
|
|
|
|
data, err := os.ReadFile(filepath.Join(dstDir, "c.txt"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(data) != "ccc" {
|
|
t.Errorf("content = %q, want %q", data, "ccc")
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPushSkipsUnchanged(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "skip-test"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0o644)
|
|
|
|
// First push
|
|
_, _ = vol.SyncPush(ctx, sid, srcDir, WithForceFull())
|
|
|
|
// Second push (no changes) without force
|
|
result, err := vol.SyncPush(ctx, sid, srcDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 0 {
|
|
t.Errorf("synced = %d, want 0 (no changes)", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestRemoteVolume(t *testing.T) {
|
|
addr := taiTestGRPC()
|
|
conn, err := grpc.NewClient(addr,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC dial %s: %v", addr, err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "sdk-remote-test"
|
|
|
|
t.Run("MkdirAll", func(t *testing.T) {
|
|
if err := vol.MkdirAll(ctx, sid, "sub/dir"); err != nil {
|
|
t.Fatalf("MkdirAll: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("WriteFile and ReadFile", func(t *testing.T) {
|
|
data := []byte("remote test content")
|
|
if err := vol.WriteFile(ctx, sid, "test.txt", data, 0o644); err != nil {
|
|
t.Fatalf("WriteFile: %v", err)
|
|
}
|
|
got, mode, err := vol.ReadFile(ctx, sid, "test.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(got) != "remote test content" {
|
|
t.Errorf("got %q", got)
|
|
}
|
|
if mode == 0 {
|
|
t.Error("mode should be nonzero")
|
|
}
|
|
})
|
|
|
|
t.Run("WriteFile empty", func(t *testing.T) {
|
|
if err := vol.WriteFile(ctx, sid, "empty.txt", []byte{}, 0o644); err != nil {
|
|
t.Fatalf("WriteFile empty: %v", err)
|
|
}
|
|
got, _, err := vol.ReadFile(ctx, sid, "empty.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if len(got) != 0 {
|
|
t.Errorf("expected empty, got %d bytes", len(got))
|
|
}
|
|
})
|
|
|
|
t.Run("Stat", func(t *testing.T) {
|
|
info, err := vol.Stat(ctx, sid, "test.txt")
|
|
if err != nil {
|
|
t.Fatalf("Stat: %v", err)
|
|
}
|
|
if info.Size != 19 {
|
|
t.Errorf("size = %d, want 19", info.Size)
|
|
}
|
|
})
|
|
|
|
t.Run("ListDir", func(t *testing.T) {
|
|
entries, err := vol.ListDir(ctx, sid, ".")
|
|
if err != nil {
|
|
t.Fatalf("ListDir: %v", err)
|
|
}
|
|
if len(entries) == 0 {
|
|
t.Error("expected entries")
|
|
}
|
|
})
|
|
|
|
t.Run("Rename", func(t *testing.T) {
|
|
if err := vol.Rename(ctx, sid, "test.txt", "renamed.txt"); err != nil {
|
|
t.Fatalf("Rename: %v", err)
|
|
}
|
|
_, _, err := vol.ReadFile(ctx, sid, "renamed.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile after rename: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Remove", func(t *testing.T) {
|
|
if err := vol.Remove(ctx, sid, "renamed.txt", false); err != nil {
|
|
t.Fatalf("Remove: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Remove recursive", func(t *testing.T) {
|
|
if err := vol.Remove(ctx, sid, "sub", true); err != nil {
|
|
t.Fatalf("RemoveAll: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("SyncPush", func(t *testing.T) {
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "push.txt"), []byte("pushed"), 0o644)
|
|
result, err := vol.SyncPush(ctx, sid, srcDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced < 1 {
|
|
t.Errorf("synced = %d", result.FilesSynced)
|
|
}
|
|
})
|
|
|
|
t.Run("SyncPull", func(t *testing.T) {
|
|
dstDir := t.TempDir()
|
|
result, err := vol.SyncPull(ctx, sid, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
if result.FilesSynced < 1 {
|
|
t.Errorf("synced = %d", result.FilesSynced)
|
|
}
|
|
// Verify pulled file content
|
|
data, err := os.ReadFile(filepath.Join(dstDir, "push.txt"))
|
|
if err != nil {
|
|
t.Fatalf("ReadFile pulled: %v", err)
|
|
}
|
|
if string(data) != "pushed" {
|
|
t.Errorf("content = %q", data)
|
|
}
|
|
})
|
|
|
|
t.Run("SyncPull with existing local files", func(t *testing.T) {
|
|
// Push a second file
|
|
_ = vol.WriteFile(ctx, sid, "extra.txt", []byte("extra"), 0o644)
|
|
|
|
dstDir := t.TempDir()
|
|
// Create a local file that matches (should be skipped)
|
|
_ = os.WriteFile(filepath.Join(dstDir, "push.txt"), []byte("pushed"), 0o644)
|
|
|
|
result, err := vol.SyncPull(ctx, sid, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
// At least extra.txt should be synced
|
|
if result.FilesSynced < 1 {
|
|
t.Errorf("synced = %d", result.FilesSynced)
|
|
}
|
|
})
|
|
|
|
t.Run("WriteFile large (multi-chunk)", func(t *testing.T) {
|
|
largeData := make([]byte, 128*1024) // 128KB > 64KB chunk
|
|
for i := range largeData {
|
|
largeData[i] = byte(i % 256)
|
|
}
|
|
if err := vol.WriteFile(ctx, sid, "large.bin", largeData, 0o644); err != nil {
|
|
t.Fatalf("WriteFile large: %v", err)
|
|
}
|
|
got, _, err := vol.ReadFile(ctx, sid, "large.bin")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile large: %v", err)
|
|
}
|
|
if len(got) != len(largeData) {
|
|
t.Errorf("len = %d, want %d", len(got), len(largeData))
|
|
}
|
|
})
|
|
|
|
t.Run("SyncPush with excludes", func(t *testing.T) {
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "keep.txt"), []byte("keep"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(srcDir, "skip.log"), []byte("skip"), 0o644)
|
|
|
|
result, err := vol.SyncPush(ctx, "exclude-remote", srcDir, WithForceFull(), WithExcludes("*.log"))
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1", result.FilesSynced)
|
|
}
|
|
_ = vol.Remove(ctx, "exclude-remote", ".", true)
|
|
})
|
|
|
|
t.Run("SyncPull empty session", func(t *testing.T) {
|
|
emptyDir := t.TempDir()
|
|
_ = vol.MkdirAll(ctx, "empty-pull", ".")
|
|
result, err := vol.SyncPull(ctx, "empty-pull", emptyDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
if result.FilesSynced != 0 {
|
|
t.Errorf("synced = %d, want 0", result.FilesSynced)
|
|
}
|
|
})
|
|
|
|
// Cleanup
|
|
_ = vol.Remove(ctx, sid, ".", true)
|
|
}
|
|
|
|
func TestLocalPathTraversal(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
// Path traversal should fail
|
|
_, _, err := vol.ReadFile(ctx, "test", "../../etc/passwd")
|
|
if err == nil {
|
|
t.Error("expected error for path traversal in ReadFile")
|
|
}
|
|
if err := vol.WriteFile(ctx, "test", "../../etc/evil", []byte("x"), 0o644); err == nil {
|
|
t.Error("expected error for path traversal in WriteFile")
|
|
}
|
|
_, err = vol.Stat(ctx, "test", "../../etc/passwd")
|
|
if err == nil {
|
|
t.Error("expected error for path traversal in Stat")
|
|
}
|
|
_, err = vol.ListDir(ctx, "test", "../../etc")
|
|
if err == nil {
|
|
t.Error("expected error for path traversal in ListDir")
|
|
}
|
|
if err := vol.Remove(ctx, "test", "../../etc/passwd", false); err == nil {
|
|
t.Error("expected error for path traversal in Remove")
|
|
}
|
|
if err := vol.Rename(ctx, "test", "../../etc/a", "b"); err == nil {
|
|
t.Error("expected error for path traversal in Rename old")
|
|
}
|
|
if err := vol.Rename(ctx, "test", "a", "../../etc/b"); err == nil {
|
|
t.Error("expected error for path traversal in Rename new")
|
|
}
|
|
if err := vol.MkdirAll(ctx, "test", "../../etc/evil"); err == nil {
|
|
t.Error("expected error for path traversal in MkdirAll")
|
|
}
|
|
}
|
|
|
|
func TestLocalReadFileNotExist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
_, _, err := vol.ReadFile(ctx, "test", "nonexistent.txt")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalStatNotExist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
_, err := vol.Stat(ctx, "test", "nonexistent.txt")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalListDirNotExist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
_, err := vol.ListDir(ctx, "test", "nonexistent")
|
|
if !os.IsNotExist(err) {
|
|
t.Errorf("expected not-exist, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLocalRemoveNotExist(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
// Non-recursive remove on nonexistent should error
|
|
err := vol.Remove(ctx, "test", "nonexistent.txt", false)
|
|
if err == nil {
|
|
t.Error("expected error for remove nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPullNoSource(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
dstDir := t.TempDir()
|
|
result, err := vol.SyncPull(ctx, "nonexistent-session", dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull nonexistent: %v", err)
|
|
}
|
|
if result.FilesSynced != 0 {
|
|
t.Errorf("synced = %d, want 0", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPushWithDirs(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "dir-sync-test"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.MkdirAll(filepath.Join(srcDir, "a", "b", "c"), 0o755)
|
|
_ = os.WriteFile(filepath.Join(srcDir, "a", "b", "c", "deep.txt"), []byte("deep"), 0o644)
|
|
|
|
result, err := vol.SyncPush(ctx, sid, srcDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestCompressDecompress(t *testing.T) {
|
|
data := []byte("hello world, this is a test of compression that needs enough data to exercise the paths")
|
|
compressed, err := compress(data)
|
|
if err != nil {
|
|
t.Fatalf("compress: %v", err)
|
|
}
|
|
decompressed, err := decompress(compressed)
|
|
if err != nil {
|
|
t.Fatalf("decompress: %v", err)
|
|
}
|
|
if string(decompressed) != string(data) {
|
|
t.Errorf("round-trip failed: got %q", decompressed)
|
|
}
|
|
}
|
|
|
|
func TestCompressLargeData(t *testing.T) {
|
|
data := make([]byte, 256*1024) // 256KB
|
|
for i := range data {
|
|
data[i] = byte(i % 256)
|
|
}
|
|
compressed, err := compress(data)
|
|
if err != nil {
|
|
t.Fatalf("compress: %v", err)
|
|
}
|
|
decompressed, err := decompress(compressed)
|
|
if err != nil {
|
|
t.Fatalf("decompress: %v", err)
|
|
}
|
|
if len(decompressed) != len(data) {
|
|
t.Errorf("len = %d, want %d", len(decompressed), len(data))
|
|
}
|
|
}
|
|
|
|
func TestDecompressInvalid(t *testing.T) {
|
|
_, err := decompress([]byte{0xFF, 0xFF, 0xFF})
|
|
if err == nil {
|
|
t.Error("expected error for invalid data")
|
|
}
|
|
}
|
|
|
|
func TestCompressEmpty(t *testing.T) {
|
|
compressed, err := compress([]byte{})
|
|
if err != nil {
|
|
t.Fatalf("compress: %v", err)
|
|
}
|
|
decompressed, err := decompress(compressed)
|
|
if err != nil {
|
|
t.Fatalf("decompress: %v", err)
|
|
}
|
|
if len(decompressed) != 0 {
|
|
t.Errorf("expected empty, got %d bytes", len(decompressed))
|
|
}
|
|
}
|
|
|
|
func TestRemoteRemoveError(t *testing.T) {
|
|
conn, err := grpc.NewClient(taiTestGRPC(),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC %s: %v", taiTestGRPC(), err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
err = vol.Remove(context.Background(), "nonexistent-session", "nonexistent.txt", false)
|
|
if err == nil {
|
|
t.Error("expected error for remove nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestRemoteRenameError(t *testing.T) {
|
|
conn, err := grpc.NewClient(taiTestGRPC(),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC %s: %v", taiTestGRPC(), err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
err = vol.Rename(context.Background(), "nonexistent-session", "a.txt", "b.txt")
|
|
if err == nil {
|
|
t.Error("expected error for rename nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestRemoteMkdirAllAndStatError(t *testing.T) {
|
|
conn, err := grpc.NewClient(taiTestGRPC(),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC %s: %v", taiTestGRPC(), err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
_, err = vol.Stat(context.Background(), "stat-test", "nonexistent.txt")
|
|
if err == nil {
|
|
t.Error("expected error for stat nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestRemoteReadFileNotFound(t *testing.T) {
|
|
conn, err := grpc.NewClient(taiTestGRPC(),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC %s: %v", taiTestGRPC(), err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
_, _, err = vol.ReadFile(context.Background(), "notfound-session", "notfound.txt")
|
|
if err == nil {
|
|
t.Error("expected error for read nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestRemoteListDirNotFound(t *testing.T) {
|
|
conn, err := grpc.NewClient(taiTestGRPC(),
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
if err != nil {
|
|
t.Skipf("gRPC %s: %v", taiTestGRPC(), err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
vol := NewRemote(conn)
|
|
_, err = vol.ListDir(context.Background(), "notfound-session", "notfound-dir")
|
|
if err == nil {
|
|
t.Error("expected error for listdir nonexistent")
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPullIncrementalSkip(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "pull-skip-test"
|
|
|
|
// Push some files
|
|
_ = vol.WriteFile(ctx, sid, "a.txt", []byte("aaa"), 0o644)
|
|
_ = vol.WriteFile(ctx, sid, "b.txt", []byte("bbb"), 0o644)
|
|
|
|
dstDir := t.TempDir()
|
|
|
|
// First pull
|
|
result1, err := vol.SyncPull(ctx, sid, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull 1: %v", err)
|
|
}
|
|
if result1.FilesSynced != 2 {
|
|
t.Errorf("first sync = %d, want 2", result1.FilesSynced)
|
|
}
|
|
|
|
// Second pull — identical mtime+size should skip
|
|
result2, err := vol.SyncPull(ctx, sid, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull 2: %v", err)
|
|
}
|
|
// Files should still be synced due to mtime possibly differing (Chtimes on first pull),
|
|
// but on the third pull they should match
|
|
result3, err := vol.SyncPull(ctx, sid, dstDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPull 3: %v", err)
|
|
}
|
|
if result3.FilesSynced != 0 {
|
|
t.Logf("sync3 = %d (may vary by platform)", result3.FilesSynced)
|
|
}
|
|
_ = result2
|
|
}
|
|
|
|
func TestLocalSyncPullWithExcludes(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "pull-excl"
|
|
|
|
_ = vol.WriteFile(ctx, sid, "keep.txt", []byte("keep"), 0o644)
|
|
_ = vol.WriteFile(ctx, sid, "skip.log", []byte("skip"), 0o644)
|
|
_ = vol.MkdirAll(ctx, sid, "node_modules")
|
|
_ = vol.WriteFile(ctx, sid, "node_modules/pkg.js", []byte("x"), 0o644)
|
|
|
|
dstDir := t.TempDir()
|
|
result, err := vol.SyncPull(ctx, sid, dstDir, WithExcludes("*.log", "node_modules"), WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPushIncremental(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "push-inc"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0o644)
|
|
|
|
// First push
|
|
result1, err := vol.SyncPush(ctx, sid, srcDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPush 1: %v", err)
|
|
}
|
|
if result1.FilesSynced != 1 {
|
|
t.Errorf("first sync = %d, want 1", result1.FilesSynced)
|
|
}
|
|
|
|
// Second push without changes — mtime matches, should skip
|
|
result2, err := vol.SyncPush(ctx, sid, srcDir)
|
|
if err != nil {
|
|
t.Fatalf("SyncPush 2: %v", err)
|
|
}
|
|
if result2.FilesSynced != 0 {
|
|
t.Logf("second sync = %d (expected 0 but may vary)", result2.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalWriteFileNested(t *testing.T) {
|
|
dir := t.TempDir()
|
|
vol := NewLocal(dir)
|
|
ctx := context.Background()
|
|
|
|
// WriteFile with deep nested path (MkdirAll should succeed)
|
|
err := vol.WriteFile(ctx, "test", "a/b/c/deep.txt", []byte("deep"), 0o644)
|
|
if err != nil {
|
|
t.Fatalf("WriteFile nested: %v", err)
|
|
}
|
|
data, _, err := vol.ReadFile(ctx, "test", "a/b/c/deep.txt")
|
|
if err != nil {
|
|
t.Fatalf("ReadFile: %v", err)
|
|
}
|
|
if string(data) != "deep" {
|
|
t.Errorf("content = %q", data)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPushExcludeDir(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.MkdirAll(filepath.Join(srcDir, ".git", "objects"), 0o755)
|
|
_ = os.WriteFile(filepath.Join(srcDir, ".git", "objects", "abc"), []byte("obj"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(srcDir, "keep.txt"), []byte("keep"), 0o644)
|
|
|
|
result, err := vol.SyncPush(ctx, "excl-dir", srcDir, WithForceFull(), WithExcludes(".git"))
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1 (exclude .git dir)", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPullForceFull(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "pull-force"
|
|
|
|
_ = vol.WriteFile(ctx, sid, "a.txt", []byte("aaa"), 0o644)
|
|
|
|
dstDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(dstDir, "a.txt"), []byte("aaa"), 0o644)
|
|
|
|
// Force full should re-sync even if same content
|
|
result, err := vol.SyncPull(ctx, sid, dstDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPull: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1 (force full)", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncPushForceFull(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
ctx := context.Background()
|
|
sid := "push-force"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "a.txt"), []byte("aaa"), 0o644)
|
|
|
|
// First sync
|
|
_, _ = vol.SyncPush(ctx, sid, srcDir)
|
|
// Force full should re-sync
|
|
result, err := vol.SyncPush(ctx, sid, srcDir, WithForceFull())
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1 (force full)", result.FilesSynced)
|
|
}
|
|
}
|
|
|
|
func TestLocalSyncExcludes(t *testing.T) {
|
|
dataDir := t.TempDir()
|
|
vol := NewLocal(dataDir)
|
|
defer vol.Close()
|
|
ctx := context.Background()
|
|
sid := "exclude-test"
|
|
|
|
srcDir := t.TempDir()
|
|
_ = os.WriteFile(filepath.Join(srcDir, "keep.txt"), []byte("k"), 0o644)
|
|
_ = os.WriteFile(filepath.Join(srcDir, "skip.log"), []byte("s"), 0o644)
|
|
|
|
result, err := vol.SyncPush(ctx, sid, srcDir, WithForceFull(), WithExcludes("*.log"))
|
|
if err != nil {
|
|
t.Fatalf("SyncPush: %v", err)
|
|
}
|
|
if result.FilesSynced != 1 {
|
|
t.Errorf("synced = %d, want 1", result.FilesSynced)
|
|
}
|
|
|
|
if _, err := os.Stat(filepath.Join(dataDir, sid, "skip.log")); !os.IsNotExist(err) {
|
|
t.Error("excluded file should not exist")
|
|
}
|
|
}
|