# test: add real-hook push helpers for checkpoint sync (I-2)

`12bd050`·  
  
Soph·1w ago·3 files·+150 added/-4 removed

The real git-invoked pre-push hook was never asserted end to end. Integration  
RunPrePush spawned the hook with Stdin=nil, and GitPush always passed  
--no-verify, so a completely broken hook would go unnoticed; e2e remote tests  
masked the same gap by always calling PushCheckpointRefs explicitly.

Integration TestEnv gains:
- InstallRealPrePushHook + GitPushWithHooks: a plain `git push` (no --no-verify)  
  that runs the installed hook exactly as git runs it — realistic stdin refspec  
  lines and remote name/URL argv, inheriting the checkpoint-store env.
- RunPrePush now feeds realistic stdin (defaultPrePushStdin) instead of nil,  
  with RunPrePushWithStdin as an escape hatch for the empty-stdin no-op case.
  GitPush keeps --no-verify for setup plumbing.

A smoke test (seed of A1) proves GitPushWithHooks lands checkpoints on a bare  
remote with NO explicit checkpoint push, under both backends via ForEachBackend.

e2e gains AssertCheckpointsOnRemote(t, s, bareDir), asserting the  
backend-appropriate refs are present on the bare remote for later use by the  
section-3 e2e tests.

## Sessions

## Changes

3

- cmd/entire/cli/integration_test

- Areal_hook_push_test.go+45

- Mtestenv.go+85/-4

- e2e/testutil

- Mrepo.go+20

```go
//go:build integration

package integration

import (
	"testing"
)

// TestGitPushWithHooks_SyncsCheckpointsToRemote is the seed of test A1: a plain
// `git push` of a feature branch, running the installed pre-push hook exactly as
// git runs it (realistic stdin refspecs, remote name/URL argv), lands the
// committed checkpoints on the bare remote WITHOUT any explicit RunPrePush or
// PushCheckpointRefs. It runs under both checkpoint backends via ForEachBackend,
// validating the whole I-1/I-2 enabler stack: env injection selects the store,
// the real hook drains it, and the backend-aware assertion finds the result.
func TestGitPushWithHooks_SyncsCheckpointsToRemote(t *testing.T) {
	t.Parallel()

ForEachBackend(t, func(t *testing.T, backend string) {
		env := NewFeatureBranchEnv(t)
		env.CheckpointStore = backend

bareDir := env.SetupBareRemote()

checkpointID := createCheckpointedCommit(t, env, "Add auth module", "auth.go", "package auth", "Add auth module")
		if checkpointID == "" {
			t.Fatal("should have a checkpoint ID after condensation")
		}

// Sanity: checkpoint exists locally under the selected backend.
		if !env.CheckpointsPresentLocally() {
			t.Fatalf("[%s] checkpoint should exist locally after condensation", backend)
		}

// Plain push through the real hook — no explicit checkpoint push.
		env.GitPushWithHooks("origin", "HEAD")

if !env.CheckpointsPresentOnRemote(bareDir) {
			t.Fatalf("[%s] checkpoints should be on remote after `git push` via the real pre-push hook", backend)
		}
		if !env.CheckpointExistsOnRemote(bareDir, checkpointID) {
			t.Fatalf("[%s] checkpoint %s should be on remote after `git push` via the real pre-push hook", backend, checkpointID)
		}
	})
}
```

```go
// GitPush pushes a branch to a remote. Fails the test on error.
// GitPush pushes a branch to a remote with --no-verify, bypassing the pre-push
// hook. Use this for setup plumbing (seeding remotes, pushing the user branch)
// where the checkpoint sync should NOT run. To exercise the real hook, use
// GitPushWithHooks. Fails the test on error.
func (env *TestEnv) GitPush(remote, refSpec string) {
	env.T.Helper()
}
```

```go
// RunPrePush runs the pre-push hook via the CLI binary, consistent with how
// other CLI invocations (GitCommitWithShadowHooks, RunCLI) use env.cliEnv().
// InstallRealPrePushHook writes .git/hooks/pre-push so a plain `git push` (no
// --no-verify) runs the checkpoint sync exactly as git runs it: git invokes the
// hook with the remote name ($1) and URL ($2) as argv and feeds
// "<local-ref> <local-sha> <remote-ref> <remote-sha>" lines on stdin. The hook
// inherits the pushing process's environment, so the checkpoint-store and git
// isolation overrides from GitPushWithHooks propagate into it.
func (env *TestEnv) InstallRealPrePushHook() {
	env.T.Helper()

hooksDir := filepath.Join(env.RepoDir, ".git", "hooks")
	if err := os.MkdirAll(hooksDir, 0o755); err != nil {
		env.T.Fatalf("failed to create hooks dir: %v", err)
	}
	// Quote the binary path so a temp path containing spaces still execs.
	script := fmt.Sprintf("#!/bin/sh\nexec %q hooks git pre-push \"$1\"\n", getTestBinary())
	hookPath := filepath.Join(hooksDir, "pre-push")
	if err := os.WriteFile(hookPath, []byte(script), 0o755); err != nil { //nolint:gosec // G306: hook scripts must be executable
		env.T.Fatalf("failed to write pre-push hook: %v", err)
	}
}
```

```go
// GitPushWithHooks pushes a branch to a remote WITHOUT --no-verify, so the
// installed pre-push hook (see InstallRealPrePushHook) runs as part of the push.
// This is the real-git path: git feeds the hook realistic stdin refspec lines
// and the remote name/URL argv, so the checkpoint sync happens without any
// explicit RunPrePush. Fails the test on error.
func (env *TestEnv) GitPushWithHooks(remote, refSpec string) {
	env.T.Helper()

env.InstallRealPrePushHook()

cmd := execx.NonInteractive(env.T.Context(), "git", "push", remote, refSpec)
	cmd.Dir = env.RepoDir
	cmd.Env = env.cliEnv()
	if output, err := cmd.CombinedOutput(); err != nil {
		env.T.Fatalf("git push (with hooks) %s %s failed: %v\n%s", remote, refSpec, err, output)
	}
}
```

```go
// RunPrePush runs the pre-push hook via the CLI binary, feeding realistic stdin
// refspec lines for the current branch (see defaultPrePushStdin). This is the
// direct-invocation stand-in for GitPushWithHooks used by tests that don't push
// the user branch. Consistent with other CLI invocations (RunCLI) it uses
// env.cliEnv().
func (env *TestEnv) RunPrePush(remote string) {
	env.T.Helper()
	if err := env.RunPrePushWithError(remote); err != nil {
	}
}
```

```go
// RunPrePushWithError runs the pre-push hook and returns any error instead of failing.
func (env *TestEnv) RunPrePushWithError(remote string) error {
	env.T.Helper()
	return env.runPrePush(remote, env.defaultPrePushStdin())
}
```

```go
// RunPrePushWithStdin runs the pre-push hook feeding the given stdin verbatim.
// Pass "" to exercise the real no-op case (a `git push` with nothing new to
// push feeds the hook empty stdin).
func (env *TestEnv) RunPrePushWithStdin(remote, stdin string) error {
	env.T.Helper()
	return env.runPrePush(remote, stdin)
}
```

```go
func (env *TestEnv) runPrePush(remote, stdin string) error {
	cmd := exec.CommandContext(env.T.Context(), getTestBinary(), "hooks", "git", "pre-push", remote)
	cmd.Dir = env.RepoDir
	cmd.Env = env.cliEnv()
	cmd.Stdin = nil
	if stdin != "" {
		cmd.Stdin = strings.NewReader(stdin)
	}

output, err := cmd.CombinedOutput()
	env.T.Logf("pre-push output: %s", output)
	return nil
}
```

```go
// defaultPrePushStdin builds the stdin line git feeds a pre-push hook for the
// current branch: "<local-ref> <local-sha> <remote-ref> <remote-sha>". The
// remote sha is all-zeros (a new branch) since it doesn't change the checkpoint
// sync behavior. Returns "" when HEAD is detached or unresolvable, so callers
// exercise the empty-stdin (no-op) case.
func (env *TestEnv) defaultPrePushStdin() string {
	branch := env.GetCurrentBranch()
	if branch == "" {
		return ""
	}
	repo, err := gitrepo.OpenPath(env.RepoDir)
	if err != nil {
		return ""
	}
	defer repo.Close()
	head, err := repo.Head()
	if err != nil {
		return ""
	}
	ref := "refs/heads/" + branch
	return fmt.Sprintf("%s %s %s %s\n", ref, head.Hash().String(), ref, plumbing.ZeroHash.String())
}
```

// FetchMetadataBranch fetches the entire/checkpoints/v1 branch from a remote URL.
// Fails the test on error. Use this for clone-and-resume tests that need metadata.
func (env *TestEnv) FetchMetadataBranch(remoteURL string) {
