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:

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

//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) {