test: add real-hook push helpers for checkpoint sync (I-2) · Entire
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: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)
}
})
}
// 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()
}
// 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)
}
}
// 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)
}
}
// 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 {
}
}
// 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())
}
// 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)
}
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
}
// 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) {