Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f8b25af
Update branchCheckpointResult to support multiple checkpoint IDs
pfleidi Feb 26, 2026
8b0bcfa
Add ParseAllCheckpoints and resumeMultipleCheckpoints
pfleidi Feb 26, 2026
0b33ed4
Merge branch 'main' of github.com:entireio/cli into squash-merge-resume
pfleidi Feb 26, 2026
c9405b9
Add integration test for multi-checkpoint resume
pfleidi Feb 26, 2026
5ddc24e
Extract displayRestoredSessions to deduplicate session display logic
pfleidi Feb 26, 2026
3c80fab
Remove redundant agent resolution gate in resumeMultipleCheckpoints
pfleidi Feb 26, 2026
c1e7316
Include error details in debug log when RestoreLogsOnly fails
pfleidi Feb 26, 2026
674adb6
Deduplicate sessions by SessionID in resumeMultipleCheckpoints
pfleidi Feb 26, 2026
e1cee2f
Extract deduplicateSessions and fix seen map staleness bug
pfleidi Feb 26, 2026
f5d7ee5
Update cmd/entire/cli/trailers/trailers_test.go
pfleidi Feb 27, 2026
d10022c
Merge branch 'main' of github.com:entireio/cli into squash-merge-resume
pfleidi Mar 2, 2026
4271b62
Sort checkpoints based on timestamp before trying to restore them.
pfleidi Mar 3, 2026
1938f48
Merge branch 'squash-merge-resume' of github.com:entireio/cli into sq…
pfleidi Mar 3, 2026
658b869
Merge branch 'main' of github.com:entireio/cli into squash-merge-resume
pfleidi Mar 3, 2026
652f3f0
Remove collectCheckpointsByAge duplication
pfleidi Mar 3, 2026
c0b9c95
Rename branchCheckpointResult since its functionality changed slightly
pfleidi Mar 3, 2026
a342bef
Update cmd/entire/cli/resume.go
pfleidi Mar 3, 2026
1764063
Update cmd/entire/cli/resume.go
pfleidi Mar 3, 2026
07eddb8
Merge branch 'squash-merge-resume' of github.com:entireio/cli into sq…
pfleidi Mar 3, 2026
8e9b25b
Remove tie breaker
pfleidi Mar 3, 2026
b0d0093
Merge branch 'main' into squash-merge-resume
pfleidi Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions cmd/entire/cli/integration_test/resume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1071,6 +1071,114 @@ func TestResume_LocalLogNoTimestamp(t *testing.T) {
}
}

// TestResume_SquashMergeMultipleCheckpoints tests resume when a squash merge commit
// contains multiple Entire-Checkpoint trailers from different sessions/commits.
// This simulates the GitHub squash merge workflow where:
// 1. Developer creates feature branch with multiple commits, each with its own checkpoint
// 2. PR is squash-merged to main, combining all commit messages (and their checkpoint trailers)
// 3. Feature branch is deleted
// 4. Running "entire resume main" should discover and restore all sessions from the squash commit
func TestResume_SquashMergeMultipleCheckpoints(t *testing.T) {
t.Parallel()
env := NewFeatureBranchEnv(t)

// === Session 1: First piece of work on feature branch ===
session1 := env.NewSession()
if err := env.SimulateUserPromptSubmit(session1.ID); err != nil {
t.Fatalf("SimulateUserPromptSubmit session1 failed: %v", err)
}

content1 := "puts 'hello world'"
env.WriteFile("hello.rb", content1)

session1.CreateTranscript(
"Create hello script",
[]FileChange{{Path: "hello.rb", Content: content1}},
)
if err := env.SimulateStop(session1.ID, session1.TranscriptPath); err != nil {
t.Fatalf("SimulateStop session1 failed: %v", err)
}

// Commit session 1 (triggers condensation → checkpoint 1 on entire/checkpoints/v1)
env.GitCommitWithShadowHooks("Create hello script", "hello.rb")
checkpointID1 := env.GetLatestCheckpointID()
t.Logf("Session 1 checkpoint: %s", checkpointID1)

// === Session 2: Second piece of work on feature branch ===
session2 := env.NewSession()
if err := env.SimulateUserPromptSubmit(session2.ID); err != nil {
t.Fatalf("SimulateUserPromptSubmit session2 failed: %v", err)
}

content2 := "puts 'goodbye world'"
env.WriteFile("goodbye.rb", content2)

session2.CreateTranscript(
"Create goodbye script",
[]FileChange{{Path: "goodbye.rb", Content: content2}},
)
if err := env.SimulateStop(session2.ID, session2.TranscriptPath); err != nil {
t.Fatalf("SimulateStop session2 failed: %v", err)
}

// Commit session 2 (triggers condensation → checkpoint 2 on entire/checkpoints/v1)
env.GitCommitWithShadowHooks("Create goodbye script", "goodbye.rb")
checkpointID2 := env.GetLatestCheckpointID()
t.Logf("Session 2 checkpoint: %s", checkpointID2)

// Verify we got two different checkpoint IDs
if checkpointID1 == checkpointID2 {
t.Fatalf("expected different checkpoint IDs, got same: %s", checkpointID1)
}

// === Simulate squash merge: switch to master, create squash commit ===
env.GitCheckoutBranch(masterBranch)

// Write the combined file changes (as if squash merged)
env.WriteFile("hello.rb", content1)
env.WriteFile("goodbye.rb", content2)
env.GitAdd("hello.rb")
env.GitAdd("goodbye.rb")

// Create squash merge commit with both checkpoint trailers in the message
// This mimics GitHub's squash merge format: PR title + individual commit messages
env.GitCommitWithMultipleCheckpoints(
"Feature branch (#1)\n\n* Create hello script\n\n* Create goodbye script",
[]string{checkpointID1, checkpointID2},
)

// Remove local session logs (simulating a fresh machine or deleted local state)
if err := os.RemoveAll(env.ClaudeProjectDir); err != nil {
t.Fatalf("failed to remove Claude project dir: %v", err)
}

// === Run resume on master ===
output, err := env.RunResume(masterBranch)
if err != nil {
t.Fatalf("resume failed: %v\nOutput: %s", err, output)
}

t.Logf("Resume output:\n%s", output)

// Should restore both sessions (multi-checkpoint path)
if !strings.Contains(output, "Restored 2 sessions") {
t.Errorf("expected 'Restored 2 sessions' in output, got: %s", output)
}

// Should contain resume commands for both sessions
if !strings.Contains(output, session1.ID) {
t.Errorf("expected session1 ID %s in output, got: %s", session1.ID, output)
}
if !strings.Contains(output, session2.ID) {
t.Errorf("expected session2 ID %s in output, got: %s", session2.ID, output)
}

// Should contain claude -r commands
if !strings.Contains(output, "claude -r") {
t.Errorf("expected 'claude -r' in output, got: %s", output)
}
}

// TestResume_RelocatedRepo tests that resume works when a repository is moved
// to a different directory after checkpoint creation. This validates that resume
// reads checkpoint data from the git metadata branch (which travels with the repo)
Expand Down
36 changes: 36 additions & 0 deletions cmd/entire/cli/integration_test/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,42 @@ func (env *TestEnv) GitCommitWithMultipleSessions(message string, sessionIDs []s
}
}

// GitCommitWithMultipleCheckpoints creates a commit with multiple Entire-Checkpoint trailers.
// This simulates a GitHub squash merge commit where multiple individual commits with
// checkpoint trailers are combined into a single commit message.
func (env *TestEnv) GitCommitWithMultipleCheckpoints(message string, checkpointIDs []string) {
env.T.Helper()

// Format message with multiple checkpoint trailers (simulating squash merge format)
var sb strings.Builder
sb.WriteString(message)
sb.WriteString("\n\n")
for _, cpID := range checkpointIDs {
sb.WriteString("Entire-Checkpoint: " + cpID + "\n")
}

repo, err := git.PlainOpen(env.RepoDir)
if err != nil {
env.T.Fatalf("failed to open git repo: %v", err)
}

worktree, err := repo.Worktree()
if err != nil {
env.T.Fatalf("failed to get worktree: %v", err)
}

_, err = worktree.Commit(sb.String(), &git.CommitOptions{
Author: &object.Signature{
Name: "Test User",
Email: "test@example.com",
When: time.Now(),
},
})
if err != nil {
env.T.Fatalf("failed to commit: %v", err)
}
}

// GetHeadHash returns the current HEAD commit hash.
func (env *TestEnv) GetHeadHash() string {
env.T.Helper()
Expand Down
Loading