# Merge pull request \#18 from entireio/changes

`b83d31d`→[main](/content/gh/entireio/git-sync/commits/main/index.html)·

pjbgf·2mo ago·6 files·+772 added/-0 removed

Expand on integration tests

## Changes

6

- internal

- planner
  
    - Mplanner_test.go+159

- syncer
  
    - Mintegration_test.go+445
    - Msyncer.go+5
    - Msyncer_test.go+64

- validation
  
    - Mvalidation.go+57
    - Mvalidation_test.go+42

```
425 unmodified lines
```

```go
// TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter is a regression
// guard for the prune-scoping rule in planner.go: --prune deletes orphan
target branches only when the user has not narrowed the source ref set
// with --branch or --map. With either filter present, branches that exist
// only on the target are out of scope and must be preserved.
func TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter(t *testing.T) {

t.Parallel()

mainHash := plumbing.NewHash("1111111111111111111111111111111111111111")
releaseHash := plumbing.NewHash("2222222222222222222222222222222222222222")

mainRef := plumbing.NewBranchReferenceName("main")
stableRef := plumbing.NewBranchReferenceName("stable")
releaseRef := plumbing.NewBranchReferenceName("release")

sourceRefs := map[plumbing.ReferenceName]plumbing.Hash{
	mainRef: mainHash,
}

tests := []struct {
	name         string
	cfg          PlanConfig
	targetRefs   map[plumbing.ReferenceName]plumbing.Hash
	wantManaged  plumbing.ReferenceName
	preservedRef plumbing.ReferenceName
}{
	{
		name: "branch filter --branch main --prune",
		cfg: PlanConfig{
			Branches: []string{"main"},
			Prune:    true,
		},
		targetRefs: map[plumbing.ReferenceName]plumbing.Hash{
			mainRef:    mainHash,
			releaseRef: releaseHash,
		},
		wantManaged:  mainRef,
		preservedRef: releaseRef,
	},
	{
		name: "rename mapping --map main:stable --prune",
		cfg: PlanConfig{
			Mappings: []RefMapping{{Source: "main", Target: "stable"}},
			Prune:    true,
		},
		targetRefs: map[plumbing.ReferenceName]plumbing.Hash{
			stableRef:  mainHash,
			releaseRef: releaseHash,
		},
		wantManaged:  stableRef,
		preservedRef: releaseRef,
	},
}

for _, tt := range tests {
	t.Run(tt.name, func(t *testing.T) {
	
t.Parallel()

desired, managed, err := BuildDesiredRefs(sourceRefs, tt.cfg)
		if err != nil {
			t.Fatalf("BuildDesiredRefs: %v", err)
		}

plans, err := BuildPlans(nil, desired, tt.targetRefs, managed, tt.cfg)
		if err != nil {
			t.Fatalf("BuildPlans: %v", err)
		}

for _, p := range plans {
			if p.TargetRef == tt.preservedRef {
				t.Fatalf("unrelated target branch %s emitted plan %+v; --prune must preserve it under filtered scope", tt.preservedRef, p)
			}
			if p.Action == ActionDelete && p.TargetRef != tt.preservedRef {
				t.Fatalf("unexpected delete plan for %s: %+v", p.TargetRef, p)
			}
		}

if _, ok := managed[tt.preservedRef]; ok {
			t.Fatalf("managed map leaked unrelated target ref %s under filtered prune scope", tt.preservedRef)
		}
		if _, ok := managed[tt.wantManaged]; !ok {
			t.Fatalf("expected managed scope to include %s, got %+v", tt.wantManaged, managed)
		}
	})
}

// More tests follow...
```

```go
// TestRun_IntegrationIncrementalPushFailureRecoversOnRetry covers the
// failure-and-retry contract for the incremental relay path. When the
// receive-pack rejects the push (here, a one-shot "ng" status), the run
// must surface an error and leave the target unchanged — receive-pack
// only commits refs that the server itself reports as ok. A retry against
// the same source/target must then drive the incremental relay to
// completion, leaving the target at the new source head.
func TestRun_IntegrationIncrementalPushFailureRecoversOnRetry(t *testing.T) {
	sourceRepo, sourceFS := newSourceRepo(t)
	makeCommits(t, sourceRepo, sourceFS, 2)
	targetRepo, _ := newSourceRepo(t)

sourceServer := newSmartHTTPRepoServer(t, sourceRepo)
	targetServer := newSmartHTTPRepoServer(t, targetRepo)
targetServer.receivePackThinCap = true
	defer sourceServer.Close()
	defer targetServer.Close()

cfg := Config{
		Source: Endpoint{URL: sourceServer.RepoURL()},
		Target: Endpoint{URL: targetServer.RepoURL()},
	}

if _, err := Run(context.Background(), cfg); err != nil {
		t.Fatalf("seed sync: %v", err)
	}

branchRef := plumbing.NewBranchReferenceName(testBranch)
	preRetryHead, err := targetRepo.Reference(branchRef, true)
	if err != nil {
		t.Fatalf("target head after seed: %v", err)
	}

// Advance source so the next sync produces a fast-forward update plan,
	// the only branch shape that takes the incremental relay path.
	makeCommits(t, sourceRepo, sourceFS, 1)
	sourceHead, err := sourceRepo.Reference(branchRef, true)
	if err != nil {
		t.Fatalf("source head: %v", err)
	}

var pushAttempts int
	targetServer.receivePackHook = func(req *packp.UpdateRequests, _ bool) *packp.ReportStatus {
		pushAttempts++
		if pushAttempts > 1 {
			return nil
		}
		report := packp.NewReportStatus()
		report.UnpackStatus = "ok"
		for _, cmd := range req.Commands {
			report.CommandStatuses = append(report.CommandStatuses, &packp.CommandStatus{
				ReferenceName: cmd.Name,
				Status:        "ng simulated incremental push failure",
			})
		}
		return report
	}

if _, err := Run(context.Background(), cfg); err == nil {
		t.Fatal("expected first incremental sync to fail under injected push rejection")
	}

afterFail, err := targetRepo.Reference(branchRef, true)
	if err != nil {
		t.Fatalf("target head after failed sync: %v", err)
	}
	if afterFail.Hash() != preRetryHead.Hash() {
		t.Fatalf("target advanced despite rejected push: pre=%s post=%s", preRetryHead.Hash(), afterFail.Hash())
	}

result, err := Run(context.Background(), cfg)
	if err != nil {
		t.Fatalf("retry after incremental failure: %v", err)
	}
	if !result.Relay || result.RelayMode != relayModeIncremental {
		t.Fatalf("expected incremental relay on retry, got %+v", result)
	}
	if result.Pushed != 1 {
		t.Fatalf("expected exactly one pushed ref on retry, got %+v", result)
	}
	if pushAttempts != 2 {
		t.Fatalf("expected exactly two receive-pack attempts (fail + retry), got %d", pushAttempts)
	}

finalHead, err := targetRepo.Reference(branchRef, true)
	if err != nil {
		t.Fatalf("target head after retry: %v", err)
	}
	if finalHead.Hash() != sourceHead.Hash() {
		t.Fatalf("target head not at source after retry: target=%s source=%s", finalHead.Hash(), sourceHead.Hash())
	}
}

// More tests follow...
```
