Fixed a path-duplication bug and added regression coverage

This commit is contained in:
2026-05-03 22:38:22 -05:00
parent d1802f99f0
commit 4252de4b99
5 changed files with 201 additions and 3 deletions

View File

@@ -4,6 +4,7 @@ import "path/filepath"
// SessionPaths contains canonical local paths for one session work directory.
type SessionPaths struct {
WorkspaceRoot string
Root string
InputsDir string
AudioDir string
@@ -26,6 +27,7 @@ func buildSessionPaths(workspaceRoot, sessionID string) SessionPaths {
root := SessionWorkDir(workspaceRoot, sessionID)
transcripts := filepath.Join(root, "transcripts")
return SessionPaths{
WorkspaceRoot: workspaceRoot,
Root: root,
InputsDir: filepath.Join(root, "inputs"),
AudioDir: filepath.Join(root, "audio"),

View File

@@ -0,0 +1,58 @@
package artifacts
import (
"os"
"path/filepath"
"strings"
)
// ResolveSessionLocalPathForRead resolves manifest/local artifact paths for read use-cases.
// Invariants for stage consumers:
// - local paths may be absolute, workspace-root qualified, or session-workdir relative.
// - callers should resolve through this helper instead of manually joining session/workdir roots.
func ResolveSessionLocalPathForRead(paths SessionPaths, localPath string) string {
p := filepath.Clean(strings.TrimSpace(localPath))
if p == "" {
return ""
}
if filepath.IsAbs(p) {
return p
}
// Already qualified against known roots.
if underRoot(p, paths.WorkspaceRoot) || underRoot(p, paths.Root) {
return p
}
candidates := []string{
p,
filepath.Clean(filepath.Join(paths.WorkspaceRoot, p)),
filepath.Clean(filepath.Join(paths.Root, p)),
}
for _, c := range candidates {
if c == "" {
continue
}
if _, err := os.Stat(c); err == nil {
return c
}
}
// Deterministic fallback for ambiguous relative values.
if strings.TrimSpace(paths.WorkspaceRoot) != "" {
return candidates[1]
}
return candidates[2]
}
func underRoot(path, root string) bool {
p := filepath.Clean(strings.TrimSpace(path))
r := filepath.Clean(strings.TrimSpace(root))
if p == "" || r == "" {
return false
}
if p == r {
return true
}
return strings.HasPrefix(p, r+string(filepath.Separator))
}

View File

@@ -0,0 +1,58 @@
package artifacts
import (
"os"
"path/filepath"
"testing"
)
func TestResolveSessionLocalPathForRead(t *testing.T) {
workspace := t.TempDir()
paths := buildSessionPaths(workspace, "s-1")
if err := os.MkdirAll(paths.TranscriptsRawDir, 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
target := filepath.Join(paths.TranscriptsRawDir, "alice.json")
if err := os.WriteFile(target, []byte(`{"segments":[]}`), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
abs := target
got := ResolveSessionLocalPathForRead(paths, abs)
if got != abs {
t.Fatalf("absolute path resolution = %q, want %q", got, abs)
}
sessionRelative := filepath.Join("transcripts", "raw", "alice.json")
got = ResolveSessionLocalPathForRead(paths, sessionRelative)
if got != target {
t.Fatalf("session-relative resolution = %q, want %q", got, target)
}
}
func TestResolveSessionLocalPathForReadRelativeWorkspaceRootQualifiedPath(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("Getwd() error = %v", err)
}
workspaceAbs := t.TempDir()
workspaceRel, err := filepath.Rel(cwd, workspaceAbs)
if err != nil {
t.Fatalf("Rel() error = %v", err)
}
paths := buildSessionPaths(workspaceRel, "s-1")
target := filepath.Join(paths.TranscriptsRawDir, "alice.json")
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
t.Fatalf("MkdirAll() error = %v", err)
}
if err := os.WriteFile(target, []byte(`{"segments":[]}`), 0o644); err != nil {
t.Fatalf("WriteFile() error = %v", err)
}
manifestPath := filepath.Join(workspaceRel, "work", "s-1", "transcripts", "raw", "alice.json")
got := ResolveSessionLocalPathForRead(paths, manifestPath)
if got != filepath.Clean(manifestPath) {
t.Fatalf("resolution = %q, want %q", got, filepath.Clean(manifestPath))
}
}