package artifacts import ( "errors" "os" "path/filepath" "strings" "testing" ) func TestEnsureLayoutCreatesExpectedDirectories(t *testing.T) { store := NewLocalStore(t.TempDir()) paths, err := store.EnsureLayoutFor("sample-campaign", "session-1") if err != nil { t.Fatalf("EnsureLayoutFor() error = %v", err) } checkDirExists(t, paths.Root) checkDirExists(t, paths.InputsDir) checkDirExists(t, paths.AudioDir) checkDirExists(t, paths.TranscriptsDir) checkDirExists(t, paths.TranscriptsRawDir) checkDirExists(t, paths.TranscriptsTrimmedDir) checkDirExists(t, paths.ArtifactsDir) checkDirExists(t, paths.ReportsDir) checkDirExists(t, paths.ConfigDir) checkDirExists(t, paths.LogsDir) checkDirExists(t, paths.CurrentDir) checkDirExists(t, paths.RunsDir) if filepath.Base(paths.ManifestPath) != "manifest.json" { t.Fatalf("ManifestPath = %q, want basename manifest.json", paths.ManifestPath) } if filepath.Base(paths.LockPath) != ".lock" { t.Fatalf("LockPath = %q, want basename .lock", paths.LockPath) } } func TestResolveSessionPathsForLegacyFallback(t *testing.T) { root := t.TempDir() store := NewLocalStore(root) legacyRoot := SessionWorkDir(root, "session-1") if err := os.MkdirAll(legacyRoot, 0o755); err != nil { t.Fatalf("MkdirAll() error = %v", err) } paths, err := store.ResolveSessionPathsFor("sample-campaign", "session-1") if err != nil { t.Fatalf("ResolveSessionPathsFor() error = %v", err) } if paths.Root != legacyRoot { t.Fatalf("paths.Root = %q, want legacy root %q", paths.Root, legacyRoot) } } func TestResolveSessionPathsForAmbiguousRoots(t *testing.T) { root := t.TempDir() store := NewLocalStore(root) legacyRoot := SessionWorkDir(root, "session-1") canonicalRoot := SessionWorkDirForCampaign(root, "sample-campaign", "session-1") for _, dir := range []string{legacyRoot, canonicalRoot} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("MkdirAll(%q) error = %v", dir, err) } } _, err := store.ResolveSessionPathsFor("sample-campaign", "session-1") if err == nil { t.Fatal("expected ambiguity error, got nil") } if !strings.Contains(err.Error(), "ambiguous session workspace roots") { t.Fatalf("error = %v, want ambiguity message", err) } } func TestChecksumCalculation(t *testing.T) { store := NewLocalStore(t.TempDir()) path := filepath.Join(t.TempDir(), "sample.txt") if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } checksum, err := store.Checksum(path) if err != nil { t.Fatalf("Checksum() error = %v", err) } const want = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" if checksum != want { t.Fatalf("checksum = %q, want %q", checksum, want) } } func TestLockAcquireRelease(t *testing.T) { store := NewLocalStore(t.TempDir()) lock, err := store.AcquireSessionLockFor("sample-campaign", "session-1") if err != nil { t.Fatalf("AcquireSessionLockFor() error = %v", err) } exists, err := store.Exists(lock.path) if err != nil { t.Fatalf("Exists() error = %v", err) } if !exists { t.Fatalf("expected lock file %q to exist", lock.path) } if err := store.ReleaseSessionLock(lock); err != nil { t.Fatalf("ReleaseSessionLock() error = %v", err) } exists, err = store.Exists(lock.path) if err != nil { t.Fatalf("Exists() error = %v", err) } if exists { t.Fatalf("expected lock file %q to be removed", lock.path) } } func TestLockConflict(t *testing.T) { store := NewLocalStore(t.TempDir()) lock1, err := store.AcquireSessionLockFor("sample-campaign", "session-1") if err != nil { t.Fatalf("first AcquireSessionLockFor() error = %v", err) } defer func() { _ = store.ReleaseSessionLock(lock1) }() _, err = store.AcquireSessionLockFor("sample-campaign", "session-1") if err == nil { t.Fatal("expected lock conflict error, got nil") } if !errors.Is(err, ErrLockConflict) { t.Fatalf("error = %v, want ErrLockConflict", err) } } func TestWriteFileAtomicAndOverwrite(t *testing.T) { store := NewLocalStore(t.TempDir()) path := filepath.Join(t.TempDir(), "out", "value.txt") if err := store.WriteFileAtomic(path, []byte("one"), 0o644); err != nil { t.Fatalf("first WriteFileAtomic() error = %v", err) } if err := store.WriteFileAtomic(path, []byte("two"), 0o644); err != nil { t.Fatalf("second WriteFileAtomic() error = %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("ReadFile() error = %v", err) } if string(data) != "two" { t.Fatalf("file content = %q, want %q", string(data), "two") } entries, err := os.ReadDir(filepath.Dir(path)) if err != nil { t.Fatalf("ReadDir() error = %v", err) } for _, e := range entries { if strings.Contains(e.Name(), ".tmp-") { t.Fatalf("unexpected temp file residue: %s", e.Name()) } } } func TestCopyInput(t *testing.T) { root := t.TempDir() store := NewLocalStore(root) srcPath := filepath.Join(t.TempDir(), "speakers.yml") content := "alice: alice.flac\n" if err := os.WriteFile(srcPath, []byte(content), 0o644); err != nil { t.Fatalf("WriteFile() error = %v", err) } ref, err := store.CopyInputFor("sample-campaign", "session-1", srcPath, "inputs/speakers.yml") if err != nil { t.Fatalf("CopyInputFor() error = %v", err) } if ref.Kind != "input" { t.Fatalf("ref.Kind = %q, want %q", ref.Kind, "input") } if ref.RelativePath != filepath.Clean("inputs/speakers.yml") { t.Fatalf("ref.RelativePath = %q, want %q", ref.RelativePath, filepath.Clean("inputs/speakers.yml")) } if !strings.HasPrefix(ref.AbsolutePath, root) { t.Fatalf("ref.AbsolutePath = %q, want under workspace root %q", ref.AbsolutePath, root) } data, err := os.ReadFile(ref.AbsolutePath) if err != nil { t.Fatalf("ReadFile() error = %v", err) } if string(data) != content { t.Fatalf("copied content = %q, want %q", string(data), content) } } func checkDirExists(t *testing.T, path string) { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("Stat(%q) error = %v", path, err) } if !info.IsDir() { t.Fatalf("%q is not a directory", path) } }