test: add shallow & partial user-clone coverage (C5, C6) · Entire
test: add shallow & partial user-clone coverage (C5, C6)
696390d·
Soph·1w ago·2 files·+174 added/-2 removed
Cover the two clone shapes a user can arrive with, across both checkpoint backends:
- C5: a
git clone --depth=1user clone runs the full session -> commit
-> real-hook push -> read flow, and the checkpoint machinery never makes
the repo shallower than git left it (no self-inflicted .git/shallow;
regressions #1443/#1276). Reading the checkpoint tolerates the shallow
boundary instead of corrupting it. - C6: a
git clone --filter=blob:noneuser clone reading a remote-only
checkpoint lazily fetches the omitted metadata blobs, and the CLI stamps
no new promisor/partialclonefilter config onto [remote "origin"]
(regression #934). The .git/config guard enforces this at cleanup; an
inline snapshot gives a clearer failure.
Refactors CloneFrom into cloneFromWithArgs so shallow/partial clone helpers can pass --depth/--filter over the smart file:// transport (required for those flags to be honored from a local bare).
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq
Sessions
4da47566b4a2View transcript
Changes
2
cmd/entire/cli/integration_test
Ashallow_partial_clone_test.go+160
- Mtestenv.go+14/-2
//go:build integration
package integration
import (
"os/exec"
"strings"
"testing"
"github.com/entireio/cli/cmd/entire/cli/testutil"
"github.com/stretchr/testify/require"
)
// C5/C6: shallow and partial USER clones. A user who cloned with
// `git clone --depth=1` or `--filter=blob:none` must still be able to enable
// Entire, run a session, commit, push through the real hook, and read
// checkpoints — without the checkpoint machinery making the repo shallower
// (self-inflicted .git/shallow, regressions #1443/#1276) or stamping promisor
// config onto the named [remote "origin"] section (regression #934). The
// .git/config guard baselined by the clone helpers fails at cleanup if origin
// gains promisor pollution beyond what git itself wrote at clone time.
// cloneShallowFrom makes a real `git clone --depth=1` of bareDir over the smart
// file:// transport (required for --depth to be honored from a local bare).
func cloneShallowFrom(t *testing.T, env *TestEnv, bareDir string) *TestEnv {
t.Helper()
return env.cloneFromWithArgs("file://"+bareDir,
[]string{"-c", "protocol.file.allow=always", "--depth=1"})
}
// clonePartialFrom makes a real `git clone --filter=blob:none` of bareDir over
// the smart file:// transport. enableFilterOnBare turns on uploadpack.allowFilter
// so the server honors the filter (git writes promisor config onto origin at
// clone time — the guard tolerates that baseline and only catches later CLI
// pollution).
func clonePartialFrom(t *testing.T, env *TestEnv, bareDir string) *TestEnv {
t.Helper()
enableFilterOnBare(t, bareDir, testutil.GitIsolatedEnv())
return env.cloneFromWithArgs("file://"+bareDir,
[]string{"-c", "protocol.file.allow=always", "--filter=blob:none"})
}
// isShallowRepo reports whether the repo at dir is shallow (has a .git/shallow
// graft). Uses git's own answer rather than probing the file so a linked
// worktree resolves correctly.
func isShallowRepo(t *testing.T, dir string) bool {
t.Helper()
cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "--is-shallow-repository")
cmd.Dir = dir
cmd.Env = testutil.GitIsolatedEnv()
out, err := cmd.Output()
require.NoError(t, err, "git rev-parse --is-shallow-repository")
return strings.TrimSpace(string(out)) == "true"
}
// TestShallowClone_SessionCommitPushResume is C5: a --depth=1 user clone can run
// the full session -> commit -> real-hook push -> resume/read flow, and the
// checkpoint machinery never makes the working repo shallower than git left it
// (no self-inflicted .git/shallow, regressions #1443/#1276).
func TestShallowClone_SessionCommitPushResume(t *testing.T) {
t.Parallel()
ForEachBackend(t, func(t *testing.T, backend string) {
seed := NewFeatureBranchEnv(t)
seed.CheckpointStore = backend
bareDir := seed.SetupBareRemote()
// Seed a couple of commits so --depth=1 genuinely truncates history.
seed.WriteFile("history.txt", "one")
seed.GitAdd("history.txt")
seed.GitCommit("history one")
seed.GitPush("origin", "HEAD")
clone := cloneShallowFrom(t, seed, bareDir)
clone.CheckpointStore = backend
require.True(t, isShallowRepo(t, clone.RepoDir),
"the clone must actually be shallow for this test to be meaningful")
checkpointID := createCheckpointedCommit(t, clone, "Shallow work", "shallow.go", "package shallow", "Shallow work")
require.NotEmpty(t, checkpointID)
clone.GitPushWithHooks("origin", "HEAD")
require.True(t, clone.CheckpointExistsOnRemote(bareDir, checkpointID),
"[%s] the checkpoint should sync to the remote from a shallow clone", backend)
// The checkpoint sync must not have deepened/re-shallowed the user repo:
// it stays shallow (git's clone-time state), never self-inflicting a new
// shallow graft or corrupting the boundary.
require.True(t, isShallowRepo(t, clone.RepoDir),
"[%s] a shallow clone must remain shallow after checkpoint sync (no self-inflicted .git/shallow churn)", backend)
// Reading the checkpoint works (auto-fetch tolerates the shallow boundary
// rather than corrupting it).
explain := clone.RunCLI("checkpoint", "explain", "--checkpoint", checkpointID)
require.Contains(t, explain, "Shallow work",
"[%s] the checkpoint should be readable from the shallow clone", backend)
})
}
// TestPartialClone_ReadFetchesBlobsNoPromisorPollution is C6: a
// --filter=blob:none user clone reading a remote-only checkpoint lazily fetches
// the metadata blobs it needs (which the partial clone omitted), and the CLI
// never stamps additional promisor/partialclonefilter config onto
// [remote "origin"] (regression #934). The .git/config guard (baselined at
// clone time) enforces the no-pollution property at cleanup; the inline check
// gives a clearer failure.
//
// The checkpoint is created and pushed by the full seed repo, so the partial
// clone genuinely lacks its blobs and must fetch them on read.
func TestPartialClone_ReadFetchesBlobsNoPromisorPollution(t *testing.T) {
t.Parallel()
ForEachBackend(t, func(t *testing.T, backend string) {
seed := NewFeatureBranchEnv(t)
seed.CheckpointStore = backend
bareDir := seed.SetupBareRemote()
// Seed repo creates a checkpoint and pushes it (branch + checkpoints).
checkpointID := createCheckpointedCommit(t, seed, "Partial work", "partial.go", "package partial", "Partial work")
require.NotEmpty(t, checkpointID)
seed.GitPush("origin", "HEAD")
seed.RunPrePush("origin")
require.True(t, seed.CheckpointExistsOnRemote(bareDir, checkpointID),
"[%s] the checkpoint should be on the remote before the partial clone reads it", backend)
// A --filter=blob:none clone gets trees but not blobs, so the checkpoint's
// metadata blobs are genuinely absent locally.
clone := clonePartialFrom(t, seed, bareDir)
clone.CheckpointStore = backend
// Snapshot origin's promisor state before the read triggers any fetch.
originPromisorBefore := originPromisorConfig(t, clone.RepoDir)
// Reading the checkpoint lazily fetches the omitted metadata blobs; it must
// succeed without pulling the whole repo.
explain := clone.RunCLI("checkpoint", "explain", "--checkpoint", checkpointID)
require.Contains(t, explain, "Partial work",
"[%s] explain should lazily fetch the metadata blobs a partial clone omitted", backend)
// The read must not have stamped new promisor config onto origin (the
// cleanup guard also enforces this; assert it inline for a clearer failure).
require.Equal(t, originPromisorBefore, originPromisorConfig(t, clone.RepoDir),
"[%s] a checkpoint read must not stamp new promisor config onto [remote \"origin\"] (#934)", backend)
})
}
// originPromisorConfig returns the promisor-related git config values for the
// named origin remote, joined for comparison. Reads only the [remote "origin"]
// keys git uses to mark a partial clone.
func originPromisorConfig(t *testing.T, dir string) string {
t.Helper()
var parts []string
for _, key := range []string{"remote.origin.promisor", "remote.origin.partialclonefilter"} {
cmd := exec.CommandContext(t.Context(), "git", "config", "--get", key)
cmd.Dir = dir
cmd.Env = testutil.GitIsolatedEnv()
out, _ := cmd.Output() // missing key -> exit 1, empty output
parts = append(parts, key+"="+strings.TrimSpace(string(out)))
}
return strings.Join(parts, " ")
}