Files
kandev/apps/backend/internal/worktree/directory_link_test.go
T

411 lines
16 KiB
Go

package worktree
import (
"os"
"path/filepath"
"strings"
"testing"
storageworkspaces "github.com/kandev/kandev/internal/system/storage/workspaces"
)
// canonicalTempDir resolves symlinks in t.TempDir() so tests hand production
// code a canonical owned control root (macOS /var -> /private/var).
func canonicalTempDir(t *testing.T) string {
t.Helper()
d, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatalf("EvalSymlinks(TempDir): %v", err)
}
return d
}
func TestCreateOwnedDirectoryLinkCreatesLiveLinkInsideOwnedRoot(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
target := filepath.Join(t.TempDir(), "source")
if err := os.MkdirAll(target, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(target, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
link, err := CreateOwnedDirectoryLink(root, "source", target)
if err != nil {
t.Fatalf("CreateOwnedDirectoryLink: %v", err)
}
if got, err := os.ReadFile(filepath.Join(link, "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("read through link = %q, %v", got, err)
}
if err := os.WriteFile(filepath.Join(target, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
if got, err := os.ReadFile(filepath.Join(link, "live.txt")); err != nil || string(got) != "two" {
t.Fatalf("link is not live: %q, %v", got, err)
}
}
func TestCreateOwnedDirectoryLinkRejectsCollision(t *testing.T) {
root := filepath.Join(t.TempDir(), "tasks", "task-1")
target := t.TempDir()
if err := os.MkdirAll(root, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(root, "source"), 0o755); err != nil {
t.Fatal(err)
}
if _, err := CreateOwnedDirectoryLink(root, "source", target); err == nil {
t.Fatal("CreateOwnedDirectoryLink succeeded for collision")
}
}
func TestCreateOwnedDirectoryLinkRejectsSymlinkedControlAncestor(t *testing.T) {
realBase := t.TempDir()
linkBase := filepath.Join(t.TempDir(), "tasks")
if err := os.Symlink(realBase, linkBase); err != nil {
t.Skipf("symlink unsupported: %v", err)
}
if _, err := CreateOwnedDirectoryLink(filepath.Join(linkBase, "task-1"), "source", t.TempDir()); err == nil {
t.Fatal("CreateOwnedDirectoryLink accepted symlinked control ancestor")
}
}
// seedOwnedDirectoryLink plants a link through the production creation path so
// the test exercises the same reparse point / symlink a real launch produced.
// Creation failure is fatal, not a skip: a directory junction needs no elevation
// on Windows and a symlink is always available on the Unix runners, so a failure
// here is a real regression rather than an unsupported platform.
func seedOwnedDirectoryLink(t *testing.T, root, name, target string) {
t.Helper()
if _, err := CreateOwnedDirectoryLink(root, name, target); err != nil {
t.Fatalf("CreateOwnedDirectoryLink: %v", err)
}
}
func testOwnedDirectoryLinkOwner() OwnedDirectoryLinkOwner {
return OwnedDirectoryLinkOwner{TaskID: "task-1", TaskDirName: "task-1"}
}
func writeOwnershipMarker(t *testing.T, root string, owner OwnedDirectoryLinkOwner) {
t.Helper()
if err := storageworkspaces.WriteOwnershipMarker(root, storageworkspaces.OwnershipMarker{
TaskID: owner.TaskID, TaskDirName: owner.TaskDirName, LayoutVersion: storageworkspaces.LayoutVersionSemantic,
}); err != nil {
t.Fatalf("WriteOwnershipMarker: %v", err)
}
}
// The predicate only reports. Every case below additionally asserts that the
// inspected entry is still on disk afterwards, since it may be a link the user
// or the repository keeps on purpose.
func TestIsSelfReferentialDirectoryLinkDetectsSelfLink(t *testing.T) {
root := canonicalTempDir(t)
seedOwnedDirectoryLink(t, root, "self", root)
selfLink, err := IsSelfReferentialDirectoryLink(root, "self")
if err != nil || !selfLink {
t.Fatalf("IsSelfReferentialDirectoryLink = %v, %v; want true, nil", selfLink, err)
}
if _, err := os.Lstat(filepath.Join(root, "self")); err != nil {
t.Fatalf("entry was removed: %v", err)
}
}
func TestIsSelfReferentialDirectoryLinkIgnoresForeignLink(t *testing.T) {
root, target := canonicalTempDir(t), t.TempDir()
if err := os.WriteFile(filepath.Join(target, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "source", target)
selfLink, err := IsSelfReferentialDirectoryLink(root, "source")
if err != nil || selfLink {
t.Fatalf("IsSelfReferentialDirectoryLink = %v, %v; want false, nil", selfLink, err)
}
if got, err := os.ReadFile(filepath.Join(root, "source", "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("read through kept link = %q, %v", got, err)
}
}
func TestIsSelfReferentialDirectoryLinkIgnoresRealDirectory(t *testing.T) {
root := t.TempDir()
real := filepath.Join(root, "api")
if err := os.Mkdir(real, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(real, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
selfLink, err := IsSelfReferentialDirectoryLink(root, "api")
if err != nil || selfLink {
t.Fatalf("IsSelfReferentialDirectoryLink = %v, %v; want false, nil", selfLink, err)
}
if got, err := os.ReadFile(filepath.Join(real, "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("real directory content = %q, %v", got, err)
}
}
func TestIsSelfReferentialDirectoryLinkIgnoresMissingEntry(t *testing.T) {
selfLink, err := IsSelfReferentialDirectoryLink(t.TempDir(), "absent")
if err != nil || selfLink {
t.Fatalf("IsSelfReferentialDirectoryLink = %v, %v; want false, nil", selfLink, err)
}
}
// Every relaunch and resume re-runs Ensure over unchanged links, so a second
// call must be a no-op. A path-text comparison rejected Windows junctions here
// because filepath.EvalSymlinks does not traverse them.
func TestEnsureOwnedDirectoryLinkIsIdempotent(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
target := t.TempDir()
if err := os.WriteFile(filepath.Join(target, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
first, err := EnsureOwnedDirectoryLink(root, "api", target, testOwnedDirectoryLinkOwner())
if err != nil || !first.Created {
t.Fatalf("first EnsureOwnedDirectoryLink: created=%v err=%v", first.Created, err)
}
second, err := EnsureOwnedDirectoryLink(root, "api", target, testOwnedDirectoryLinkOwner())
if err != nil {
t.Fatalf("second EnsureOwnedDirectoryLink: %v; an unchanged link must be reused", err)
}
if second.Created {
t.Fatal("second EnsureOwnedDirectoryLink reported creation")
}
if second.Path != first.Path {
t.Fatalf("second link = %q, want %q", second.Path, first.Path)
}
if got, err := os.ReadFile(filepath.Join(second.Path, "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("read through reused link = %q, %v", got, err)
}
}
// A Kandev-owned task root is Kandev's to reconcile: an owned directory link is
// a pointer, not content, so a stale target left by an earlier launch must be
// repointed rather than wedging every launch and resume with a target mismatch.
func TestEnsureOwnedDirectoryLinkRepointsOwnedLinkOnMismatch(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
target, other := t.TempDir(), t.TempDir()
if err := os.WriteFile(filepath.Join(other, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", target)
result, err := EnsureOwnedDirectoryLink(root, "api", other, testOwnedDirectoryLinkOwner())
if err != nil {
t.Fatalf("EnsureOwnedDirectoryLink repoint: %v", err)
}
if !result.Created {
t.Fatal("EnsureOwnedDirectoryLink did not report a recreate on mismatch")
}
linkInfo, err := os.Stat(result.Path)
if err != nil {
t.Fatalf("stat repointed link: %v", err)
}
otherInfo, err := os.Stat(other)
if err != nil {
t.Fatalf("stat new target: %v", err)
}
if !os.SameFile(linkInfo, otherInfo) {
t.Fatal("link was not repointed to the new target")
}
if got, err := os.ReadFile(filepath.Join(result.Path, "live.txt")); err != nil || string(got) != "two" {
t.Fatalf("read through repointed link = %q, %v", got, err)
}
}
func TestEnsureOwnedDirectoryLinkRepointsOwnedLinkWithMatchingMarker(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current, other := t.TempDir(), t.TempDir()
if err := os.WriteFile(filepath.Join(other, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
writeOwnershipMarker(t, root, testOwnedDirectoryLinkOwner())
result, err := EnsureOwnedDirectoryLink(root, "api", other, testOwnedDirectoryLinkOwner())
if err != nil {
t.Fatalf("EnsureOwnedDirectoryLink with matching marker: %v", err)
}
if !result.Created || result.PriorTarget == "" {
t.Fatalf("result = %+v, want Created with a prior target", result)
}
if got, err := os.ReadFile(filepath.Join(result.Path, "live.txt")); err != nil || string(got) != "two" {
t.Fatalf("read through matching-marker repoint = %q, %v", got, err)
}
}
func TestEnsureOwnedDirectoryLinkRejectsMarkerConflictOnMismatch(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current, other := t.TempDir(), t.TempDir()
if err := os.WriteFile(filepath.Join(current, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(other, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
writeOwnershipMarker(t, root, testOwnedDirectoryLinkOwner())
_, err := EnsureOwnedDirectoryLink(root, "api", other, OwnedDirectoryLinkOwner{TaskID: "task-2", TaskDirName: "task-2"})
if err == nil || !strings.Contains(err.Error(), errWorkspaceOwnershipMarkerConflict) {
t.Fatalf("EnsureOwnedDirectoryLink error = %v, want marker conflict", err)
}
if got, err := os.ReadFile(filepath.Join(root, "api", "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("marker-conflict repoint disturbed original link = %q, %v", got, err)
}
}
func TestEnsureOwnedDirectoryLinkRejectsTraversalBeforeRepoint(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current, other := t.TempDir(), t.TempDir()
if err := os.WriteFile(filepath.Join(current, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(other, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
if _, err := EnsureOwnedDirectoryLink(root, "../api", other, testOwnedDirectoryLinkOwner()); err == nil {
t.Fatal("EnsureOwnedDirectoryLink accepted a traversal entry name")
}
if got, err := os.ReadFile(filepath.Join(root, "api", "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("traversal attempt disturbed original link = %q, %v", got, err)
}
}
func TestRemoveOwnedDirectoryLinkRemovesDirectoryLink(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
target := t.TempDir()
seedOwnedDirectoryLink(t, root, "notes", target)
if err := RemoveOwnedDirectoryLink(root, "notes"); err != nil {
t.Fatalf("RemoveOwnedDirectoryLink: %v", err)
}
if _, err := os.Lstat(filepath.Join(root, "notes")); !os.IsNotExist(err) {
t.Fatalf("removed directory link still exists: %v", err)
}
}
func TestRemoveOwnedDirectoryLinkPreservesNonLinkEntry(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
path := filepath.Join(root, "notes")
const contents = "user content"
if err := os.MkdirAll(root, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatal(err)
}
if err := RemoveOwnedDirectoryLink(root, "notes"); err == nil {
t.Fatal("RemoveOwnedDirectoryLink removed a non-link entry")
}
if got, err := os.ReadFile(path); err != nil || string(got) != contents {
t.Fatalf("non-link entry = %q, %v; want it preserved", got, err)
}
}
func TestEnsureOwnedDirectoryLinkKeepsOriginalLinkWhenReplacementTargetIsInvalid(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current := t.TempDir()
if err := os.WriteFile(filepath.Join(current, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
if _, err := EnsureOwnedDirectoryLink(root, "api", filepath.Join(t.TempDir(), "missing"), testOwnedDirectoryLinkOwner()); err == nil {
t.Fatal("EnsureOwnedDirectoryLink accepted a missing replacement target")
}
if got, err := os.ReadFile(filepath.Join(root, "api", "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("failed replacement disturbed original link = %q, %v", got, err)
}
}
func TestRestoreOwnedDirectoryLinkKeepsCurrentLinkWhenReplacementTargetIsInvalid(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current := t.TempDir()
if err := os.WriteFile(filepath.Join(current, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
if err := RestoreOwnedDirectoryLink(root, "api", filepath.Join(t.TempDir(), "missing")); err == nil {
t.Fatal("RestoreOwnedDirectoryLink accepted a missing replacement target")
}
if got, err := os.ReadFile(filepath.Join(root, "api", "live.txt")); err != nil || string(got) != "one" {
t.Fatalf("failed restore disturbed current link = %q, %v", got, err)
}
}
func TestRenameInspectedDirectoryLinkRejectsChangedEntry(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
current, other := t.TempDir(), t.TempDir()
if err := os.WriteFile(filepath.Join(current, "live.txt"), []byte("one"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(other, "live.txt"), []byte("two"), 0o644); err != nil {
t.Fatal(err)
}
seedOwnedDirectoryLink(t, root, "api", current)
inspected, err := os.Lstat(filepath.Join(root, "api"))
if err != nil {
t.Fatalf("Lstat(api): %v", err)
}
tempLink := filepath.Join(root, "api.tmp")
if err := createPlatformDirectoryLink(other, tempLink); err != nil {
t.Fatalf("create temp replacement link: %v", err)
}
if err := os.Remove(filepath.Join(root, "api")); err != nil {
t.Fatalf("remove original link: %v", err)
}
if err := os.Mkdir(filepath.Join(root, "api"), 0o755); err != nil {
t.Fatalf("create replacement directory: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "api", "keep.txt"), []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}
if err := renameInspectedDirectoryLink(tempLink, filepath.Join(root, "api"), inspected); err == nil {
t.Fatal("renameInspectedDirectoryLink accepted a changed entry")
}
if got, err := os.ReadFile(filepath.Join(root, "api", "keep.txt")); err != nil || string(got) != "keep" {
t.Fatalf("changed entry was disturbed = %q, %v", got, err)
}
}
// A non-link entry (a real file or directory a reconcile did not create) is not
// Kandev's pointer to replace: it stays fail-closed and is never removed.
func TestEnsureOwnedDirectoryLinkRejectsNonLinkEntry(t *testing.T) {
root := filepath.Join(canonicalTempDir(t), "tasks", "task-1")
if err := os.MkdirAll(filepath.Join(root, "api"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "api", "keep.txt"), []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := EnsureOwnedDirectoryLink(root, "api", t.TempDir(), testOwnedDirectoryLinkOwner()); err == nil {
t.Fatal("EnsureOwnedDirectoryLink overwrote a non-link entry")
}
if got, err := os.ReadFile(filepath.Join(root, "api", "keep.txt")); err != nil || string(got) != "keep" {
t.Fatalf("non-link entry was disturbed = %q, %v", got, err)
}
// A regular file occupying the entry name is likewise not Kandev's pointer to
// replace: it stays fail-closed and its bytes must survive untouched.
file := filepath.Join(root, "web")
if err := os.WriteFile(file, []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := EnsureOwnedDirectoryLink(root, "web", t.TempDir(), testOwnedDirectoryLinkOwner()); err == nil {
t.Fatal("EnsureOwnedDirectoryLink overwrote a non-link file")
}
if got, err := os.ReadFile(file); err != nil || string(got) != "keep" {
t.Fatalf("non-link file was disturbed = %q, %v", got, err)
}
}