From b200241b11511145ec18910b8367b83e5c218004 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sat, 4 Jul 2026 22:37:28 +0200 Subject: [PATCH 1/6] test: add OPF pre-push rewrite coverage over a real remote (E1-E5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the OPF (OpenAI Privacy Filter) pre-push rewrite at the integration and strategy layers, exercising what the existing in-process unit tests structurally cannot: OPF enabled via committed settings, resolved and executed inside a spawned pre-push hook, redacting real committed checkpoints and pushing the Entire-OPF-Applied trailer to a real bare remote. - E1: happy path via the real git-invoked hook — remote v1 tip carries the trailer, the fake opf binary is invoked, checkpoint + branch land. - E2: diverged v1 -> V1DivergedError aborts the user push (non-zero exit, actionable message); the branch tip on the remote does not advance. - E4: bootstrap cap exceeded -> typed abort before OPF runs; nothing pushed and the opf binary is never executed. - E5: non-TTY push (execx.NonInteractive, no controlling terminal) completes without hanging and still ships the trailer (regression 626a0344e). - E3: the CAS V1RefMovedError path is deterministically impossible to hit from a spawned hook, so it lands at the strategy layer via a fake OPF runtime that advances the local v1 ref mid-rewrite; a re-run succeeds. Integration tests point the OPF `command` at a fake opf script on disk (the in-process runtime seam is unavailable across the process boundary). OPF is git-branch only, so all E-group tests stay on the default backend. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: 950ada63c568 --- .../cli/integration_test/opf_prepush_test.go | 248 ++++++++++++++++++ .../manual_commit_opf_rewrite_test.go | 72 +++++ 2 files changed, 320 insertions(+) create mode 100644 cmd/entire/cli/integration_test/opf_prepush_test.go diff --git a/cmd/entire/cli/integration_test/opf_prepush_test.go b/cmd/entire/cli/integration_test/opf_prepush_test.go new file mode 100644 index 0000000000..eefcd80246 --- /dev/null +++ b/cmd/entire/cli/integration_test/opf_prepush_test.go @@ -0,0 +1,248 @@ +//go:build integration + +package integration + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/entireio/cli/cmd/entire/cli/paths" + "github.com/entireio/cli/cmd/entire/cli/testutil" + "github.com/stretchr/testify/require" +) + +// E-group: OPF pre-push rewrite over a real remote via the real git-invoked +// pre-push hook. OPF is git-branch only (the git-refs pre-push path descopes +// it), so every test here stays on the default git-branch backend. +// +// The unit tests in strategy/manual_commit_opf_rewrite_test.go cover the +// rewrite plumbing in-process with a fake runtime. These tests cover what the +// unit tests structurally cannot: OPF enabled via committed settings, resolved +// and executed inside a spawned hook subprocess, redacting real committed +// checkpoints and pushing the Entire-OPF-Applied trailer to a real bare remote. +// +// The spawned hook constructs a real redact.shellOut from settings and execs +// the configured `command`, so these tests point that command at a fake `opf` +// script on disk (writeFakeOPFScript). The in-process ConfigurePrivacyFilterWithRuntime +// seam the unit tests use is unavailable across the process boundary. + +// writeFakeOPFScript writes an executable stand-in for the `opf` binary and +// returns its path. The real binary reads the concatenated batch on stdin and +// emits {"detected_spans":[...]} on stdout; this fake reads and discards stdin, +// records each invocation to markerFile (so a test can prove the subprocess +// actually ran), and reports no detected spans. Reporting no spans is enough to +// exercise the whole pipeline: a successful (exit 0, parseable) run means the +// rewrite tags commits Entire-OPF-Applied and pushes them, whereas a missing or +// broken binary would trip the circuit breaker and abort the push with +// OPFRuntimeFailedError. Content-level redaction correctness is covered by the +// strategy unit tests. +func writeFakeOPFScript(t *testing.T, markerFile string) string { + t.Helper() + dir := t.TempDir() + if resolved, err := filepath.EvalSymlinks(dir); err == nil { + dir = resolved + } + scriptPath := filepath.Join(dir, "fake-opf") + // Consume all of stdin (avoid SIGPIPE on the writer), record the call, emit + // an empty-but-valid typed-JSON result. + script := "#!/bin/sh\n" + + "cat > /dev/null\n" + + "echo invoked >> " + shellQuote(markerFile) + "\n" + + "printf '%s' '{\"detected_spans\":[]}'\n" + require.NoError(t, os.WriteFile(scriptPath, []byte(script), 0o755)) //nolint:gosec // fake binary must be executable + return scriptPath +} + +// shellQuote single-quotes s for safe embedding in a /bin/sh script. +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} + +// enableOPF turns on the OpenAI Privacy Filter in the repo's committed settings, +// pointing the runtime at the fake opf script. prompt_default=always so the +// non-interactive hook runs OPF without prompting. +func enableOPF(t *testing.T, env *TestEnv, opfCommand string) { + t.Helper() + env.PatchSettings(map[string]any{ + "redaction": map[string]any{ + "openai_privacy_filter": map[string]any{ + "enabled": true, + "categories": map[string]any{"private_person": true}, + "command": opfCommand, + "prompt_default": "always", + }, + }, + }) +} + +// remoteMetadataTipMessage returns the full commit message of the remote v1 +// branch tip. Reads the bare repo directly to avoid testing production code +// with itself. +func remoteMetadataTipMessage(t *testing.T, bareDir string) string { + t.Helper() + cmd := exec.CommandContext(t.Context(), "git", "log", "-1", "--format=%B", "refs/heads/"+paths.MetadataBranchName) + cmd.Dir = bareDir + cmd.Env = testutil.GitIsolatedEnv() + out, err := cmd.Output() + require.NoError(t, err, "read remote v1 tip message") + return string(out) +} + +// remoteBranchTip returns the commit hash the given branch points at on the +// bare remote, or "" if the branch is absent. Reads the bare repo directly. +func remoteBranchTip(t *testing.T, bareDir, branch string) string { + t.Helper() + cmd := exec.CommandContext(t.Context(), "git", "rev-parse", "--verify", "--quiet", "refs/heads/"+branch) + cmd.Dir = bareDir + cmd.Env = testutil.GitIsolatedEnv() + out, _ := cmd.Output() // non-zero exit (missing ref) yields empty output + return strings.TrimSpace(string(out)) +} + +// TestOPFPrePush_HappyPath_TrailerLandsOnRemote is E1: with OPF enabled in +// committed settings, a plain `git push` runs the real pre-push hook, which +// resolves OPF from settings, rewrites the unpushed v1 commits with the +// Entire-OPF-Applied trailer, and pushes them. The remote v1 tip carries the +// trailer, the fake opf binary was actually invoked, and the user branch lands. +func TestOPFPrePush_HappyPath_TrailerLandsOnRemote(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + + marker := filepath.Join(t.TempDir(), "opf-invocations") + enableOPF(t, env, writeFakeOPFScript(t, marker)) + + checkpointID := createCheckpointedCommit(t, env, "Add auth module", "auth.go", "package auth", "Add auth module") + require.NotEmpty(t, checkpointID, "should have a checkpoint after condensation") + + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + require.NoError(t, err, "user push with OPF enabled should succeed:\n%s", out) + + // The rewritten remote v1 tip carries the OPF-applied trailer. + require.Contains(t, remoteMetadataTipMessage(t, bareDir), "Entire-OPF-Applied: true", + "remote v1 tip should carry the Entire-OPF-Applied trailer after the OPF pre-push rewrite") + + // The fake opf binary was actually executed by the spawned hook (proves OPF + // ran rather than being silently skipped). + data, readErr := os.ReadFile(marker) //nolint:gosec // path built from test temp dir + require.NoError(t, readErr, "opf marker file should exist (opf binary should have been invoked)") + require.NotEmpty(t, strings.TrimSpace(string(data)), "opf binary should have recorded at least one invocation") + + // The checkpoint and the user branch both reached the remote. + require.True(t, env.CheckpointExistsOnRemote(bareDir, checkpointID), + "checkpoint should be on the remote after the OPF pre-push") + require.True(t, env.BranchExistsOnRemote(bareDir, "feature/test-branch"), + "user feature branch should land on the remote alongside the rewritten checkpoints") +} + +// TestOPFPrePush_DivergedV1_AbortsUserPush is E2: when OPF is enabled and local +// v1 has diverged from the remote (a shared base, then both sides added a +// checkpoint), the rewrite refuses with V1DivergedError before running OPF, and +// that error propagates out of the hook so `git push` exits non-zero with the +// actionable message. The user branch does NOT land. +func TestOPFPrePush_DivergedV1_AbortsUserPush(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + + marker := filepath.Join(t.TempDir(), "opf-invocations") + // A missing binary would be fine here too — divergence is detected before + // OPF runs — but a real script keeps the setup uniform. + enableOPF(t, env, writeFakeOPFScript(t, marker)) + + // Shared base checkpoint, pushed so local and remote v1 align. + baseCP := createCheckpointedCommit(t, env, "base work", "base.go", "package base", "base work") + require.NotEmpty(t, baseCP) + env.GitPush("origin", "HEAD") + env.RunPrePush("origin") + require.True(t, env.CheckpointExistsOnRemote(bareDir, baseCP), "base checkpoint should be on remote") + + // Both sides add a checkpoint from the shared base -> diverged v1. + _ = createCheckpointedCommit(t, env, "local diverge", "diverge.go", "package diverge", "local diverge") + _ = advanceRemoteV1(t, env, bareDir, "remote diverge work") + + // A pushable feature commit so git actually invokes the hook. + env.WriteFile("trigger.txt", "x") + env.GitAdd("trigger.txt") + env.GitCommit("push trigger") + + // Record the remote's feature-branch tip before the aborted push so we can + // prove the push transferred nothing (the branch itself was seeded by + // SetupBareRemote, so its mere presence is not evidence either way). + tipBefore := remoteBranchTip(t, bareDir, "feature/test-branch") + + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + require.Error(t, err, "a diverged v1 under OPF must abort the user push") + require.Contains(t, out, "diverged", + "the push failure should surface the actionable divergence message:\n%s", out) + + require.Equal(t, tipBefore, remoteBranchTip(t, bareDir, "feature/test-branch"), + "an aborted OPF push must not advance the user branch on the remote") +} + +// TestOPFPrePush_BootstrapCapAbortsPush is E4: a first push to a remote with no +// v1 yet, whose local history exceeds ENTIRE_OPF_BOOTSTRAP_LIMIT, aborts with a +// typed BootstrapTooLargeError before OPF runs, and nothing is pushed. The cap +// is set to 1 while creating two checkpoints, so the second push's unpushed set +// exceeds it. +func TestOPFPrePush_BootstrapCapAbortsPush(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + + marker := filepath.Join(t.TempDir(), "opf-invocations") + enableOPF(t, env, writeFakeOPFScript(t, marker)) + // Cap the bootstrap at 1 commit; two checkpoints (2 v1 commits) exceed it. + env.ExtraEnv = append(env.ExtraEnv, "ENTIRE_OPF_BOOTSTRAP_LIMIT=1") + + _ = createCheckpointedCommit(t, env, "first work", "one.go", "package one", "first work") + _ = createCheckpointedCommit(t, env, "second work", "two.go", "package two", "second work") + + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + require.Error(t, err, "an over-cap OPF bootstrap must abort the push") + require.Contains(t, out, "bootstrap", + "the push failure should surface the bootstrap-cap remediation message:\n%s", out) + + require.False(t, env.CheckpointsPresentOnRemote(bareDir), + "no checkpoints should reach the remote when the bootstrap cap aborts the push") + + // OPF itself is never invoked: the cap fires before the shell-out. + _, statErr := os.Stat(marker) + require.True(t, os.IsNotExist(statErr), + "the bootstrap cap must abort before the opf binary is ever executed") +} + +// TestOPFPrePush_NonTTY_CompletesWithoutHang is E5 (regression 626a0344e): a +// non-interactive push with OPF enabled must complete without any /dev/tty +// access and without hanging. GitPushArgsWithHooks runs git via execx.NonInteractive, +// which puts the child in a new session with no controlling terminal, so any +// stray /dev/tty read would fail rather than block. The push must finish well +// inside a generous wall-clock bound and land the trailer. +func TestOPFPrePush_NonTTY_CompletesWithoutHang(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + + marker := filepath.Join(t.TempDir(), "opf-invocations") + enableOPF(t, env, writeFakeOPFScript(t, marker)) + + _ = createCheckpointedCommit(t, env, "Add module", "mod.go", "package mod", "Add module") + + start := time.Now() + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + elapsed := time.Since(start) + + require.NoError(t, err, "non-TTY OPF push should succeed without a controlling terminal:\n%s", out) + require.Less(t, elapsed, 60*time.Second, + "non-TTY OPF push must not hang on a /dev/tty read (took %s)", elapsed) + require.Contains(t, remoteMetadataTipMessage(t, bareDir), "Entire-OPF-Applied: true", + "the non-TTY OPF push should still rewrite and push the trailer") +} diff --git a/cmd/entire/cli/strategy/manual_commit_opf_rewrite_test.go b/cmd/entire/cli/strategy/manual_commit_opf_rewrite_test.go index 875e08b4fd..c565212b24 100644 --- a/cmd/entire/cli/strategy/manual_commit_opf_rewrite_test.go +++ b/cmd/entire/cli/strategy/manual_commit_opf_rewrite_test.go @@ -74,6 +74,36 @@ func findSentinelSpans(s string) []redact.Span { return spans } +// fakeOPFMovesRefOnce redacts nothing (empty spans) but, on its FIRST +// RedactBatch call, invokes onFirst — a hook the test uses to advance the +// local v1 ref mid-rewrite, simulating another worktree writing a checkpoint +// between the OPF shell-out and the CAS ref update. This is the deterministic +// integration-impossible seam for the V1RefMovedError path: at integration +// level the rewrite runs inside a spawned hook subprocess, so there is no way +// to interpose a concurrent ref move at exactly the right instant. Injecting +// the move through the runtime here reproduces the CAS race precisely. +type fakeOPFMovesRefOnce struct { + mu sync.Mutex + moved bool + onFirst func() +} + +func (f *fakeOPFMovesRefOnce) Redact(_ context.Context, _ string, _ []string) ([]redact.Span, error) { + return nil, nil +} + +func (f *fakeOPFMovesRefOnce) RedactBatch(_ context.Context, inputs []string, _ []string) ([][]redact.Span, error) { + f.mu.Lock() + if !f.moved && f.onFirst != nil { + f.moved = true + f.onFirst() + } + f.mu.Unlock() + // Empty spans: no OPF redaction, but the runtime succeeds so the rewrite + // proceeds to the CAS step (where the injected ref move trips it). + return make([][]redact.Span, len(inputs)), nil +} + // fakeRuntimeAlwaysFails trips the OPF circuit breaker on first call. // Used to test the fail-closed assertion that breaker-trip during // rewrite aborts before CAS. @@ -567,6 +597,48 @@ func TestCollectTreeBlobs_RedactsAllFileTypes(t *testing.T) { require.False(t, collectedNames[paths.ContentHashFileName], "content_hash.txt must be excluded from collection (deferred path)") } +// E3 (strategy layer): a concurrent local ref move between the OPF shell-out +// and the CAS ref update surfaces as V1RefMovedError, and a clean re-run then +// succeeds. This is the strategy-level home of test-plan E3: the pre-push hook +// runs the rewrite in a spawned subprocess, so the CAS race can only be hit +// deterministically here, by advancing the local v1 ref from inside the OPF +// runtime (fakeOPFMovesRefOnce.onFirst). The integration E-group notes this +// split. +func TestRewriteUnpushedV1WithOPF_RefMovedDuringRewrite_ThenRetrySucceeds(t *testing.T) { + fake := &fakeOPFMovesRefOnce{} + configureFakeOPF(t, fake) + repo, originalTip := setupV1Repo(t) + + // Between the OPF batch call and atomicSetV1Ref, "another worktree" writes a + // new checkpoint, advancing local v1 off originalTip. + fake.onFirst = func() { + addV1Checkpoint(t, repo, "b2c3d4e5f6a7", "concurrent-session", + "A concurrent session mentions PERSONABC", "Concurrent PERSONABC work") + } + + _, err := RewriteUnpushedV1WithOPF(context.Background(), repo, "origin") + var moved *V1RefMovedError + require.ErrorAs(t, err, &moved, "want V1RefMovedError when the local v1 ref moves mid-rewrite, got %T: %v", err, err) + require.Equal(t, moved.Expected, originalTip, + "V1RefMovedError.Expected should be the tip captured at the start of the rewrite") + require.NotEqual(t, originalTip, moved.Actual, + "V1RefMovedError.Actual should be the ref's new position after the concurrent write") + + // A re-run sees the moved ref as the new baseline; no further move is + // injected, so the CAS matches and the push proceeds. + newTip, err := RewriteUnpushedV1WithOPF(context.Background(), repo, "origin") + require.NoError(t, err, "the retry after a V1RefMovedError should succeed") + require.False(t, newTip.IsZero()) + retryCommit, err := repo.CommitObject(newTip) + require.NoError(t, err) + require.True(t, trailers.HasOPFApplied(retryCommit.Message), + "the retried rewrite should tag the new tip Entire-OPF-Applied") + + ref, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) + require.NoError(t, err) + require.Equal(t, newTip, ref.Hash(), "local v1 ref should point at the retried rewrite's tip") +} + // Fail-closed regression: when the OPF runtime fails and the breaker // trips, the rewrite must NOT CAS the ref. Otherwise the new commits // would carry Entire-OPF-Applied: true while their content is 7-layer From b9e9dc92f7211a0b93d7bfc0b6593536a3020803 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sat, 4 Jul 2026 22:44:30 +0200 Subject: [PATCH 2/6] test: add degraded-remote & protocol-edge coverage (F1, F2, F4, F5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - F1 (git-branch): a bare-remote pre-receive hook rejecting the checkpoint v1 branch with a GH013-style message drives the protected-ref path: the hook prints its loud one-shot banner and returns, the user's own push still lands, and a repeat push does not retry-loop the rejected branch onto the remote (regression #1033). - F2 (git-refs slice): pushCheckpointRefWithRecovery wraps its push, fetch+replay, and retry in the same shared checkpointPushBudget as the v1 path, so a stuck transport bounds total wall clock to ~one budget rather than stacking a full budget per attempt. Mirrors the existing git-branch TestDoPushRef_SharedBudget_BoundsTotalWallClock (regressions #1282, 2e2c1b73a). - F4 (git-refs): the git-refs pre-push path honors the checkpoint policy — a local policy this CLI cannot satisfy skips the per-checkpoint ref push (leaving refs queued) but does NOT abort the user push (regression 7bbdad09c follow-up). - F5 (git-branch): a session that checkpoints while HEAD is detached, pushed via `git push origin HEAD:` through the real hook, syncs and is readable from a fresh clone. Pins current (working) behavior. F6 (entire:// provider-host routing) is deferred: it needs an injectable provider host table / fake provider mapping that does not yet exist. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: ca5c18be1b8a --- .../integration_test/degraded_remotes_test.go | 186 ++++++++++++++++++ .../strategy/push_common_budget_unix_test.go | 65 ++++++ 2 files changed, 251 insertions(+) create mode 100644 cmd/entire/cli/integration_test/degraded_remotes_test.go diff --git a/cmd/entire/cli/integration_test/degraded_remotes_test.go b/cmd/entire/cli/integration_test/degraded_remotes_test.go new file mode 100644 index 0000000000..c5c88a847e --- /dev/null +++ b/cmd/entire/cli/integration_test/degraded_remotes_test.go @@ -0,0 +1,186 @@ +//go:build integration + +package integration + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/entireio/cli/cmd/entire/cli/checkpointpolicy" + "github.com/entireio/cli/cmd/entire/cli/gitrepo" + "github.com/entireio/cli/cmd/entire/cli/testutil" + "github.com/go-git/go-git/v6/plumbing" + "github.com/stretchr/testify/require" +) + +// F-group: degraded remotes & protocol edges. + +// installProtectV1BranchHook writes a pre-receive hook on the bare remote that +// rejects any update to refs/heads/entire/checkpoints/v1 with a GH013-style +// message (the shape classifyPushOutput recognizes as a protected-ref +// rejection). Non-checkpoint refs (the user's branch) are accepted, so the +// hook models a repository ruleset that protects the entire/* namespace. +func installProtectV1BranchHook(t *testing.T, bareDir string) { + t.Helper() + hooksDir := filepath.Join(bareDir, "hooks") + require.NoError(t, os.MkdirAll(hooksDir, 0o755)) + script := "#!/bin/sh\n" + + "while read -r _old _new ref; do\n" + + " case \"$ref\" in\n" + + " refs/heads/entire/checkpoints/v1)\n" + + " echo 'remote: error: GH013: Repository rule violations found for refs/heads/entire/checkpoints/v1' >&2\n" + + " echo 'remote: Cannot update this protected ref.' >&2\n" + + " exit 1;;\n" + + " esac\n" + + "done\n" + + "exit 0\n" + require.NoError(t, os.WriteFile(filepath.Join(hooksDir, "pre-receive"), []byte(script), 0o755)) //nolint:gosec // hook must be executable +} + +// TestProtectedV1Branch_UserPushUnaffected is F1 (regression #1033): when the +// remote rejects the checkpoint v1 branch with a GH013-style protected-ref +// error, the pre-push hook prints a loud one-shot banner and returns without +// retrying — the user's own push still succeeds. git-branch only (the v1 +// branch is git-branch's checkpoint ref). +func TestProtectedV1Branch_UserPushUnaffected(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + installProtectV1BranchHook(t, bareDir) + + _ = createCheckpointedCommit(t, env, "Add auth", "auth.go", "package auth", "Add auth") + + // A fresh pushable commit so git actually invokes the pre-push hook and has + // something to transfer on the user branch. + env.WriteFile("feature.txt", "feature") + env.GitAdd("feature.txt") + env.GitCommit("feature commit") + + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + require.NoError(t, err, "user push must succeed even when the checkpoint v1 branch is protected:\n%s", out) + + // The loud protected-ref banner is surfaced (not a silent failure). + require.Contains(t, out, "BLOCKED: remote rejected push", + "a protected-ref rejection should print the loud banner:\n%s", out) + + // The checkpoint branch was rejected — it must not be on the remote. + require.False(t, env.CheckpointsPresentOnRemote(bareDir), + "the protected v1 branch must not land on the remote") + + // The user branch advanced to the new commit (their push was unaffected). + localHead := localGitRevParse(t, env, "HEAD") + require.Equal(t, localHead, remoteBranchTip(t, bareDir, "feature/test-branch"), + "the user's own push should land the feature commit despite the checkpoint rejection") + + // No retry loop: a second push with nothing new for the user still succeeds + // fast and does not smuggle the checkpoint branch onto the remote. + out2, err2 := env.GitPushArgsWithHooks("origin", "HEAD") + require.NoError(t, err2, "a repeat push must still succeed:\n%s", out2) + require.False(t, env.CheckpointsPresentOnRemote(bareDir), + "the protected v1 branch must still be absent after a repeat push") +} + +// localGitRevParse resolves a revision to its commit hash in the working repo. +func localGitRevParse(t *testing.T, env *TestEnv, rev string) string { + t.Helper() + cmd := exec.CommandContext(env.T.Context(), "git", "rev-parse", rev) + cmd.Dir = env.RepoDir + cmd.Env = testutil.GitIsolatedEnv() + out, err := cmd.Output() + require.NoError(t, err, "git rev-parse %s", rev) + return string(out[:len(out)-1]) // strip trailing newline +} + +// seedBlockedCheckpointPolicy writes a local checkpoint policy whose +// checkpoint_min_version this CLI cannot read (branch-v2), so +// CanSatisfyPolicy is false. Because the bare remote carries no policy ref, +// syncCheckpointPolicyForPrePush leaves this local policy in place, which makes +// checkpointPolicyAllowsGitHook return false at pre-push time. +func seedBlockedCheckpointPolicy(t *testing.T, env *TestEnv) { + t.Helper() + repo, err := gitrepo.OpenPath(env.RepoDir) + require.NoError(t, err) + _, err = checkpointpolicy.WriteLocal(env.T.Context(), repo, plumbing.ZeroHash, checkpointpolicy.Policy{ + CheckpointVersion: "branch-v2", + CheckpointMinVersion: "branch-v2", + }) + require.NoError(t, err, "seed blocked local checkpoint policy") +} + +// TestGitRefsBlockedPolicy_SkipsRefPushNotUserPush is F4 (regression 7bbdad09c +// follow-up): the git-refs pre-push path honors the checkpoint policy. A local +// policy this CLI cannot satisfy skips the per-checkpoint ref push (leaving the +// refs queued for a later, upgraded run) but does NOT abort the user's push. +func TestGitRefsBlockedPolicy_SkipsRefPushNotUserPush(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + env.CheckpointStore = StoreGitRefs + bareDir := env.SetupBareRemote() + + checkpointID := createCheckpointedCommit(t, env, "Add service", "svc.go", "package svc", "Add service") + require.NotEmpty(t, checkpointID) + ref := checkpointRefName(checkpointID) + require.Contains(t, env.PushQueueRefs(), ref, "the new checkpoint ref should be queued before push") + + // Block the policy right before the push. + seedBlockedCheckpointPolicy(t, env) + + out, err := env.GitPushArgsWithHooks("origin", "HEAD") + require.NoError(t, err, "a blocked checkpoint policy must not abort the user push:\n%s", out) + + // User branch landed; checkpoint ref did not; queue still holds the ref. + require.True(t, env.BranchExistsOnRemote(bareDir, "feature/test-branch"), + "the user branch should land even though the policy blocked checkpoint sync") + require.False(t, env.CheckpointExistsOnRemote(bareDir, checkpointID), + "a blocked policy must skip pushing the checkpoint ref") + require.Contains(t, env.PushQueueRefs(), ref, + "a blocked policy must leave the checkpoint ref queued for a later retry") +} + +// detachHead detaches HEAD at its current commit (a git-plumbing operation the +// TestEnv has no direct helper for). Subsequent commits advance the detached +// HEAD without moving any branch. +func detachHead(t *testing.T, env *TestEnv) { + t.Helper() + cmd := exec.CommandContext(env.T.Context(), "git", "checkout", "--detach", "HEAD") + cmd.Dir = env.RepoDir + cmd.Env = testutil.GitIsolatedEnv() + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git checkout --detach failed: %v\n%s", err, output) + } +} + +// TestDetachedHead_CheckpointPushAndResume is F5: a session that runs and +// checkpoints while HEAD is detached, pushed with `git push origin HEAD:` +// via the real hook, is discoverable and readable after a fresh clone. This +// pins the current behavior. git-branch (default) backend. +func TestDetachedHead_CheckpointPushAndResume(t *testing.T) { + t.Parallel() + + env := NewFeatureBranchEnv(t) + bareDir := env.SetupBareRemote() + + detachHead(t, env) + + checkpointID := createCheckpointedCommit(t, env, "Detached work", "detached.go", "package detached", "Detached work") + require.NotEmpty(t, checkpointID, "a checkpoint should be created while HEAD is detached") + require.True(t, env.CheckpointsPresentLocally(), "checkpoint should exist locally after a detached-HEAD session") + + // Push the detached work to a named branch on the remote through the real + // pre-push hook (git feeds the hook a "HEAD refs/heads/detached-work " + // stdin line). + out, err := env.GitPushArgsWithHooks("origin", "HEAD:refs/heads/detached-work") + require.NoError(t, err, "pushing detached HEAD to a branch via the real hook should succeed:\n%s", out) + require.True(t, env.CheckpointExistsOnRemote(bareDir, checkpointID), + "the detached-HEAD checkpoint should sync to the remote") + + // A fresh clone can read the checkpoint (auto-fetch on read). + clone := env.CloneFrom(bareDir) + explain := clone.RunCLI("checkpoint", "explain", "--checkpoint", checkpointID) + require.Contains(t, explain, "Detached work", + "the detached-HEAD checkpoint should be readable from a fresh clone") +} diff --git a/cmd/entire/cli/strategy/push_common_budget_unix_test.go b/cmd/entire/cli/strategy/push_common_budget_unix_test.go index 89f6de3883..37f61a8e79 100644 --- a/cmd/entire/cli/strategy/push_common_budget_unix_test.go +++ b/cmd/entire/cli/strategy/push_common_budget_unix_test.go @@ -10,7 +10,9 @@ import ( "time" "github.com/entireio/cli/cmd/entire/cli/paths" + "github.com/entireio/cli/cmd/entire/cli/testutil" + "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing" "github.com/stretchr/testify/require" ) @@ -55,3 +57,66 @@ func TestDoPushRef_SharedBudget_BoundsTotalWallClock(t *testing.T) { require.GreaterOrEqual(t, elapsed, budget/2, "push should have run until the budget deadline; took %s", elapsed) } + +// F2 (git-refs slice): the per-checkpoint push path +// (pushCheckpointRefWithRecovery) wraps its initial push, fetch+replay, and +// retry in the SAME shared checkpointPushBudget as the v1 path. A stuck +// transport must therefore bound the total wall clock to ~one budget, not +// stack a full budget per attempt. This is the git-refs analogue of +// TestDoPushRef_SharedBudget_BoundsTotalWallClock (git-branch/v1). +// +// Not parallel: uses t.Setenv, t.Chdir, and overrides checkpointPushBudget. +func TestPushCheckpointRefWithRecovery_SharedBudget_BoundsTotalWallClock(t *testing.T) { + const budget = 2 * time.Second + restoreBudget := checkpointPushBudget + checkpointPushBudget = budget + t.Cleanup(func() { checkpointPushBudget = restoreBudget }) + + hangScript := filepath.Join(t.TempDir(), "hang.sh") + require.NoError(t, os.WriteFile(hangScript, []byte("#!/bin/sh\nexec sleep 30\n"), 0o755)) + t.Setenv("GIT_SSH_COMMAND", hangScript) + t.Setenv("ENTIRE_CHECKPOINT_TOKEN", "") + + tmpDir := setupRepoWithCheckpointRef(t) + t.Chdir(tmpDir) + + restoreStderr := captureStderr(t) + defer restoreStderr() + + const target = "ssh://git@localhost/checkpoints.git" + ref := plumbing.ReferenceName("refs/entire/checkpoints/ab/abcdef012345") + + start := time.Now() + err := pushCheckpointRefWithRecovery(context.Background(), target, ref) + elapsed := time.Since(start) + + // The recovery path surfaces the error (its caller keeps the ref queued); + // what matters here is that it returns bounded by the shared budget. + require.Error(t, err, "a stuck transport should surface an error from the recovery path") + require.Less(t, elapsed, 5*time.Second, + "pushCheckpointRefWithRecovery should return at ~budget, not stack per-attempt timeouts; took %s", elapsed) + require.GreaterOrEqual(t, elapsed, budget/2, + "the push should have run until the budget deadline; took %s", elapsed) +} + +// setupRepoWithCheckpointRef builds a repo with one per-checkpoint ref +// (refs/entire/checkpoints//) pointing at HEAD, so the git-refs push +// path has a real local ref to push. +func setupRepoWithCheckpointRef(t *testing.T) string { + t.Helper() + + tmpDir := t.TempDir() + testutil.InitRepo(t, tmpDir) + testutil.WriteFile(t, tmpDir, "f.txt", "init") + testutil.GitAdd(t, tmpDir, "f.txt") + testutil.GitCommit(t, tmpDir, "init") + + repo, err := git.PlainOpen(tmpDir) + require.NoError(t, err) + head, err := repo.Head() + require.NoError(t, err) + + ref := plumbing.ReferenceName("refs/entire/checkpoints/ab/abcdef012345") + require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(ref, head.Hash()))) + return tmpDir +} From 696390d2d4f1fb75f4668fd04b7e3c761177170c Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sat, 4 Jul 2026 22:50:47 +0200 Subject: [PATCH 3/6] test: add shallow & partial user-clone coverage (C5, C6) Cover the two clone shapes a user can arrive with, across both checkpoint backends: - C5: a `git clone --depth=1` user 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:none` user 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 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: 4da47566b4a2 --- .../shallow_partial_clone_test.go | 160 ++++++++++++++++++ cmd/entire/cli/integration_test/testenv.go | 16 +- 2 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 cmd/entire/cli/integration_test/shallow_partial_clone_test.go diff --git a/cmd/entire/cli/integration_test/shallow_partial_clone_test.go b/cmd/entire/cli/integration_test/shallow_partial_clone_test.go new file mode 100644 index 0000000000..d0e2ca6218 --- /dev/null +++ b/cmd/entire/cli/integration_test/shallow_partial_clone_test.go @@ -0,0 +1,160 @@ +//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, " ") +} diff --git a/cmd/entire/cli/integration_test/testenv.go b/cmd/entire/cli/integration_test/testenv.go index 0ddce21243..ab7455471b 100644 --- a/cmd/entire/cli/integration_test/testenv.go +++ b/cmd/entire/cli/integration_test/testenv.go @@ -1852,6 +1852,17 @@ func (env *TestEnv) SetupNamedBareRemote(remoteName string) string { // The clone checks out the same branch as the current env's HEAD. func (env *TestEnv) CloneFrom(bareDir string) *TestEnv { env.T.Helper() + return env.cloneFromWithArgs(bareDir, nil) +} + +// cloneFromWithArgs is CloneFrom with an explicit clone source and extra `git +// clone` flags/config, so callers can produce shallow (--depth) or partial +// (--filter=blob:none) user clones. cloneSource may be a local path or a +// file:// URL; extraArgs are inserted before the source (e.g. "-c", +// "protocol.file.allow=always", "--filter=blob:none"). The resulting TestEnv is +// otherwise identical to CloneFrom's (own .entire, git config guard baselined). +func (env *TestEnv) cloneFromWithArgs(cloneSource string, extraArgs []string) *TestEnv { + env.T.Helper() ctx := env.T.Context() @@ -1867,14 +1878,15 @@ func (env *TestEnv) CloneFrom(bareDir string) *TestEnv { // Bare repos may have HEAD pointing to a non-existent default branch // when the original was on a feature branch. cloneArgs := []string{"clone"} + cloneArgs = append(cloneArgs, extraArgs...) if currentBranch != "" { cloneArgs = append(cloneArgs, "--branch", currentBranch) } - cloneArgs = append(cloneArgs, bareDir, cloneDir) + cloneArgs = append(cloneArgs, cloneSource, cloneDir) cmd := exec.CommandContext(ctx, "git", cloneArgs...) cmd.Env = testutil.GitIsolatedEnv() if output, err := cmd.CombinedOutput(); err != nil { - env.T.Fatalf("failed to clone from %s: %v\n%s", bareDir, err, output) + env.T.Fatalf("failed to clone from %s: %v\n%s", cloneSource, err, output) } // Configure git user (clone doesn't inherit local config from the bare repo) From 7093889ad4b39426b17313d85541a9b1a9623dae Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sat, 4 Jul 2026 23:16:03 +0200 Subject: [PATCH 4/6] test(e2e): add worktree-remote and unreachable-remote scenarios (G2, G3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - G2: an agent session runs inside a linked `git worktree`, the user commits and pushes the worktree's branch with a plain `git push` (the real pre-push hook syncs checkpoints — no explicit PushCheckpointRefs), and a fresh clone `entire resume`s the session. Exercises the worktree shadow-branch namespace and the shared common-dir push queue end to end. - G3: `entire doctor` on a repo whose origin points at an unreachable (hermetic, missing local path) target completes without hanging or crashing and still reports its local session-health scan — pinning the degrade-to-local behavior. Both are vogon-only (deterministic, free, part of the CI canary matrix). G2 reuses the resume-from-clone prompt phrasing verbatim so vogon's regex prompt parser needs no changes. Adds entire.DoctorCtx, a context-bounded doctor runner that returns output+error instead of failing the test, so G3 can surface a hang as a deadline. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: 8f2fa44f0ab7 --- e2e/entire/entire.go | 12 +++ e2e/tests/worktree_remote_test.go | 151 ++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 e2e/tests/worktree_remote_test.go diff --git a/e2e/entire/entire.go b/e2e/entire/entire.go index 00e0b1745c..efa75efe3f 100644 --- a/e2e/entire/entire.go +++ b/e2e/entire/entire.go @@ -53,6 +53,18 @@ func Doctor(t *testing.T, dir string) string { return run(t, dir, "doctor", "--force") } +// DoctorCtx runs `entire doctor --force` under the given context and returns the +// combined output and any error WITHOUT failing the test. Use it to pin doctor's +// behavior against a degraded/unreachable remote: pass a context.WithTimeout so a +// hang surfaces as context.DeadlineExceeded rather than blocking the suite. +func DoctorCtx(ctx context.Context, dir string) (string, error) { + cmd := execx.NonInteractive(ctx, BinPath(), "doctor", "--force") + cmd.Dir = dir + cmd.Env = os.Environ() + out, err := cmd.CombinedOutput() + return strings.TrimSpace(string(out)), err +} + // CleanDryRun runs `entire clean --dry-run` and returns the output. func CleanDryRun(t *testing.T, dir string) string { t.Helper() diff --git a/e2e/tests/worktree_remote_test.go b/e2e/tests/worktree_remote_test.go new file mode 100644 index 0000000000..e3e73284f4 --- /dev/null +++ b/e2e/tests/worktree_remote_test.go @@ -0,0 +1,151 @@ +//go:build e2e + +package tests + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/entireio/cli/e2e/entire" + "github.com/entireio/cli/e2e/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestResumeFromWorktreeSession is G2: an agent session runs inside a linked +// `git worktree`, the user commits and pushes the worktree's branch with a plain +// `git push` (the real pre-push hook syncs checkpoints — no explicit +// PushCheckpointRefs), and a fresh clone elsewhere can `entire resume` the +// session. This exercises the worktree shadow-branch namespace and the shared +// (common-dir) push queue end to end. +// +// vogon-only: deterministic and free, matching the doctor canary. The prompt +// phrasing is copied verbatim from the resume-from-clone test so vogon's +// regex-based prompt parser needs no changes. +func TestResumeFromWorktreeSession(t *testing.T) { + testutil.ForEachNamedAgent(t, 3*time.Minute, []string{"vogon"}, func(t *testing.T, s *testutil.RepoState, ctx context.Context) { + bareDir := testutil.SetupBareRemote(t, s) + + // Commit the `entire enable` files on the default branch and push, so the + // bare remote has a base the clone can start from. + s.Git(t, "add", ".") + s.Git(t, "commit", "-m", "Enable entire") + s.Git(t, "push") + + // Create a linked worktree on a feature branch. It shares the main repo's + // git common dir (objects, refs, hooks, and the checkpoint push queue). + worktreeDir := filepath.Join(t.TempDir(), "worktree") + if resolved, symErr := filepath.EvalSymlinks(filepath.Dir(worktreeDir)); symErr == nil { + worktreeDir = filepath.Join(resolved, "worktree") + } + s.Git(t, "worktree", "add", worktreeDir, "-b", "feature") + + // .entire/ is gitignored, so the worktree's working tree has none — enable + // Entire in the worktree to give it its own settings (hooks live in the + // shared common dir and are already installed). + entire.Enable(t, worktreeDir, s.Agent.EntireAgent()) + testutil.PatchSettings(t, worktreeDir, map[string]any{"log_level": "debug", "commit_linking": "always"}) + + // Run the agent IN the worktree (its cwd is the worktree, so the session + // transcript and shadow branch are scoped to it). + out, err := s.Agent.RunPrompt(ctx, worktreeDir, + "create a file at docs/hello.md with a paragraph about greetings. Do not ask for confirmation, just make the change.") + s.ConsoleLog.WriteString("> [worktree] " + out.Command + "\nstdout:\n" + out.Stdout + "\nstderr:\n" + out.Stderr + "\n") + require.NoError(t, err, "agent failed in worktree") + + testutil.Git(t, worktreeDir, "add", ".") + testutil.Git(t, worktreeDir, "commit", "-m", "Add hello doc") + + // Checkpoint refs live in the shared common dir, visible from s.Dir. + testutil.WaitForCheckpoint(t, s, 30*time.Second) + checkpointID := testutil.AssertHasCheckpointTrailer(t, worktreeDir, "HEAD") + require.NotEmpty(t, checkpointID, "worktree commit should carry a checkpoint trailer") + sessionMeta := testutil.WaitForSessionMetadata(t, s.Dir, checkpointID, 0, 30*time.Second) + + // Plain push of the worktree's branch through the real hook drains the + // shared push queue / syncs the v1 branch — no explicit PushCheckpointRefs. + testutil.Git(t, worktreeDir, "push", "-u", "origin", "feature") + testutil.AssertCheckpointsOnRemote(t, s, bareDir) + + // Clone elsewhere (a teammate) and resume the worktree's session. + cloneDir := t.TempDir() + if resolved, symErr := filepath.EvalSymlinks(cloneDir); symErr == nil { + cloneDir = resolved + } + require.NoError(t, os.RemoveAll(cloneDir)) + testutil.Git(t, "", "clone", bareDir, cloneDir) + testutil.Git(t, cloneDir, "config", "user.name", "E2E Clone") + testutil.Git(t, cloneDir, "config", "user.email", "e2e-clone@test.local") + + require.False(t, testutil.CheckpointsPresent(cloneDir), + "checkpoint metadata should not exist locally in the clone before resume") + + // Materialize the feature branch locally, then return to the default branch + // so resume can switch to it. + mainClone := testutil.GitOutput(t, cloneDir, "branch", "--show-current") + testutil.Git(t, cloneDir, "checkout", "feature") + testutil.Git(t, cloneDir, "checkout", mainClone) + + entire.Enable(t, cloneDir, s.Agent.EntireAgent()) + testutil.CommitIfDirty(t, cloneDir, "Enable entire in clone") + + resumeOut, err := entire.Resume(cloneDir, "feature") + require.NoError(t, err, "entire resume failed in clone: %s", resumeOut) + + current := testutil.GitOutput(t, cloneDir, "branch", "--show-current") + assert.Equal(t, "feature", current, "should be on the feature branch after resume") + assert.Contains(t, resumeOut, "To continue", "resume output should show resume instructions") + assert.True(t, testutil.CheckpointsPresent(cloneDir), + "checkpoint metadata should exist locally after resuming the worktree session") + + if restoredTranscript, ok := testutil.RestoredSessionTranscriptPath(t, cloneDir, sessionMeta); ok { + _, statErr := os.Stat(restoredTranscript) + assert.NoError(t, statErr, "restored session transcript should exist at %s after resume", restoredTranscript) + } + }) +} + +// TestDoctorUnreachableRemote is G3: `entire doctor` on a repo whose origin +// points at an unreachable target reports the situation and exits without +// hanging or crashing. It pins the current behavior: doctor degrades to +// local-only checks rather than blocking on the dead remote. +func TestDoctorUnreachableRemote(t *testing.T) { + testutil.ForEachNamedAgent(t, 3*time.Minute, []string{"vogon"}, func(t *testing.T, s *testutil.RepoState, ctx context.Context) { + // A committed checkpoint so doctor has real metadata/session state to scan. + s.Git(t, "add", ".") + s.Git(t, "commit", "-m", "Enable entire") + + _, err := s.RunPrompt(t, ctx, + "create a file at docs/doctor.md with a short paragraph about checkpoint health. Do not ask for confirmation or approval, just make the change.") + require.NoError(t, err, "agent failed") + + s.Git(t, "add", ".") + s.Git(t, "commit", "-m", "Add doctor coverage doc") + testutil.WaitForCheckpoint(t, s, 30*time.Second) + + // Point origin at a local path that does not exist — unreachable but fully + // hermetic (no network) and fails fast. + deadRemote := filepath.Join(t.TempDir(), "nonexistent-bare.git") + s.Git(t, "remote", "add", "origin", deadRemote) + + // Bound the run so a hang surfaces as a deadline rather than blocking. + dctx, cancel := context.WithTimeout(ctx, 90*time.Second) + defer cancel() + + out, doctorErr := entire.DoctorCtx(dctx, s.Dir) + t.Logf("doctor (unreachable remote) exit=%v\n%s", doctorErr, out) + + require.False(t, errors.Is(dctx.Err(), context.DeadlineExceeded), + "doctor must not hang on an unreachable remote") + require.NotContains(t, out, "panic:", "doctor must not crash on an unreachable remote") + + // Pin: doctor still completes its local scan and reports session health + // despite the dead remote (it degrades rather than aborting the scan). + assert.Contains(t, out, "stuck sessions", + "doctor should still report the session-health scan even with an unreachable remote") + }) +} From 2f04953ddb1bcd16984c74f7856e1c4a45ab3a7a Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sat, 4 Jul 2026 23:17:30 +0200 Subject: [PATCH 5/6] docs: mark test plan implementation status Groups A/C/D/E/F(1,2,4,5)/G delivered on this branch; B-group blocked on decision D-1, F6 deferred, two pinned behaviors await product decisions. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: 4f990d0112c0 --- docs/testing/git-remote-test-plan.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/testing/git-remote-test-plan.md b/docs/testing/git-remote-test-plan.md index 8692a8776c..0b294f0a1f 100644 --- a/docs/testing/git-remote-test-plan.md +++ b/docs/testing/git-remote-test-plan.md @@ -1,6 +1,15 @@ # Test Plan: Git Remote Operations & Upstream Resolution — Both Checkpoint Backends -Status: proposal, 2026-07-04. +Status: implemented on this branch, 2026-07-04 (originally a proposal; kept as the +coverage map). Delivered: infrastructure I-1/I-2/I-4, groups A, C (incl. C5/C6), +D, E, F1/F2/F4/F5, G1–G3, plus two fixes surfaced along the way (a +graceful-degradation test false-positive and explain's git-refs fallback masking +fetch errors as "checkpoint not found"). Still open: the B-group (blocked on +decision D-1 below), F6 (needs an injectable provider host table), and the two +pinned behaviors awaiting product decisions — the >1000-commit replay cap +(`TestCollectCommitsSince_CommitCapPinsCurrentBehavior`; the historical fix +4cf01edb3 was never merged) and the two-remote queue-clearing gap +(`TestGitRefsQueue_TwoRemotesQueueClearedByFirstPush`, decision D-2). Scope: e2e (`e2e/tests/`) + integration (`cmd/entire/cli/integration_test/`) coverage for everything that talks to a git remote — pre-push checkpoint sync, remote/upstream resolution, cross-machine fetch — for the **git-branch** (`entire/checkpoints/v1`) and **git-refs** (`refs/entire/checkpoints//`) backends. --- From 2182fe09c3f0170a6086ee38424d57756a77d772 Mon Sep 17 00:00:00 2001 From: Stefan Haubold Date: Sun, 5 Jul 2026 10:15:40 +0200 Subject: [PATCH 6/6] test: hoist -c config before the clone subcommand (Copilot review) Global -c applies to the transport for the invocation only; `git clone -c` would also persist the setting into the clone's .git/config. Matches the `git -c protocol.file.allow=always fetch` pattern used elsewhere. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012yi3hHGAGepwfrfjPETjGq Entire-Checkpoint: 9d3594469d76 --- cmd/entire/cli/integration_test/testenv.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cmd/entire/cli/integration_test/testenv.go b/cmd/entire/cli/integration_test/testenv.go index 35c9790b56..f88d24dd65 100644 --- a/cmd/entire/cli/integration_test/testenv.go +++ b/cmd/entire/cli/integration_test/testenv.go @@ -1874,11 +1874,25 @@ func (env *TestEnv) cloneFromWithArgs(cloneSource string, extraArgs []string) *T // Get the current branch name to clone the right branch currentBranch := env.GetCurrentBranch() + // Hoist "-c key=val" pairs before the clone subcommand: as a global git + // option the config applies to the transport for this invocation only, + // whereas `git clone -c` also persists it into the clone's .git/config. + // Everything else stays a clone flag (--depth, --filter, ...). + var globalArgs, cloneFlags []string + for i := 0; i < len(extraArgs); i++ { + if extraArgs[i] == "-c" && i+1 < len(extraArgs) { + globalArgs = append(globalArgs, "-c", extraArgs[i+1]) + i++ + continue + } + cloneFlags = append(cloneFlags, extraArgs[i]) + } + // Clone the bare repo, explicitly checking out the right branch. // Bare repos may have HEAD pointing to a non-existent default branch // when the original was on a feature branch. - cloneArgs := []string{"clone"} - cloneArgs = append(cloneArgs, extraArgs...) + cloneArgs := append(globalArgs, "clone") + cloneArgs = append(cloneArgs, cloneFlags...) if currentBranch != "" { cloneArgs = append(cloneArgs, "--branch", currentBranch) }