Files
stack-sync/main_test.go
T
LyAhn b80713b334 feat(sync): replace hub with an internal branch engine
Fetch and prune the preferred remote, then reconcile every local branch
independently. Fast-forward branches that are behind, preserve divergent work,
and delete a branch only when its configured upstream is gone and its commits
are already contained by the remote default branch.

Keep dirty repositories eligible while protecting the active branch and refs
checked out in linked worktrees. Preserve the previous all-clean policy behind
--strict, expose structured per-branch results, add real bare-remote integration
tests, and retain the upstream MIT notice for the adapted hub sync behavior.
2026-09-06 19:41:53 +01:00

157 lines
4.4 KiB
Go

package main
import (
"bytes"
"io"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
)
func git(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
}
func makeRepo(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(path, 0o755); err != nil {
t.Fatal(err)
}
git(t, path, "init", "-q")
git(t, path, "config", "user.name", "Test")
git(t, path, "config", "user.email", "test@example.com")
if err := os.WriteFile(filepath.Join(path, "tracked.txt"), []byte("clean\n"), 0o644); err != nil {
t.Fatal(err)
}
git(t, path, "add", "tracked.txt")
git(t, path, "commit", "-qm", "initial")
git(t, path, "remote", "add", "origin", "https://example.invalid/repo.git")
}
func TestDiscoverNestedRepositories(t *testing.T) {
root := t.TempDir()
parent := filepath.Join(root, "parent")
child := filepath.Join(parent, "apps", "child")
makeRepo(t, parent)
makeRepo(t, child)
paths, err := discover(root, nil)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 || paths[0] != parent || paths[1] != child {
t.Fatalf("discover() = %#v", paths)
}
}
func TestNestedRepoDoesNotDirtyParent(t *testing.T) {
root := t.TempDir()
parent := filepath.Join(root, "parent")
child := filepath.Join(parent, "child")
makeRepo(t, parent)
makeRepo(t, child)
paths, _ := discover(root, nil)
got := inspect(root, parent, paths)
if !got.Eligible {
t.Fatalf("parent blocked by nested repo: %+v", got)
}
if len(got.NestedRepoEntries) == 0 {
t.Fatalf("expected nested repository entry to be recorded: %+v", got)
}
}
func TestDirtyFileIsReportedButRepositoryRemainsEligible(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
if err := os.WriteFile(filepath.Join(path, "tracked.txt"), []byte("changed\n"), 0o644); err != nil {
t.Fatal(err)
}
got := inspect(root, path, []string{path})
if !got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("dirty repository was not eligible with its changes reported: %+v", got)
}
}
func TestUntrackedFileAlongsideNestedRepoIsStillReported(t *testing.T) {
root := t.TempDir()
parent := filepath.Join(root, "parent")
child := filepath.Join(parent, "child")
makeRepo(t, parent)
makeRepo(t, child)
if err := os.WriteFile(filepath.Join(parent, "notes.txt"), []byte("work\n"), 0o644); err != nil {
t.Fatal(err)
}
paths, _ := discover(root, nil)
got := inspect(root, parent, paths)
if !got.Eligible || len(got.Dirty) != 1 {
t.Fatalf("untracked file should be reported without blocking parent: %+v", got)
}
}
func TestSkippedDirectoriesAreNotSearched(t *testing.T) {
root := t.TempDir()
makeRepo(t, filepath.Join(root, "node_modules", "dependency"))
makeRepo(t, filepath.Join(root, "real"))
paths, err := discover(root, nil)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 || paths[0] != filepath.Join(root, "real") {
t.Fatalf("discover() = %#v", paths)
}
}
func TestStrictSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
planned := inspect(root, path, []string{path})
if !planned.Eligible {
t.Fatalf("fixture should initially be eligible: %+v", planned)
}
if err := os.WriteFile(filepath.Join(path, "late-change.txt"), []byte("do not touch\n"), 0o644); err != nil {
t.Fatal(err)
}
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true, true)
if len(results) != 1 || !results[0].Skipped || results[0].Success {
t.Fatalf("syncAll() = %+v", results)
}
}
func TestScanReportsDirtyEligibleRepository(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, "repo")
makeRepo(t, path)
if err := os.WriteFile(filepath.Join(path, "untracked.txt"), []byte("work\n"), 0o644); err != nil {
t.Fatal(err)
}
var stdout, stderr bytes.Buffer
code := run([]string{"scan", "--root", root}, bytes.NewReader(nil), &stdout, &stderr)
if code != 0 {
t.Fatalf("run() code = %d, stderr = %q", code, stderr.String())
}
if !bytes.Contains(stdout.Bytes(), []byte("dirty worktree (1 changes), checked-out branch protected")) {
t.Fatalf("scan did not report dirty eligible repository:\n%s", stdout.String())
}
}
func TestIndentNormalizesGitProgressOutput(t *testing.T) {
got := indent("first\rsecond\r\nthird", " ")
want := " first\n second\n third"
if got != want {
t.Fatalf("indent() = %q, want %q", got, want)
}
}