fix: guard adopt against shared session stores · Entire
fix: guard adopt against shared session stores
40b8864
Sessions
c555c03f3864 View transcript
--enable hooks and amake sure entire is enabled in this session and you're up to date on latest version and main pleaseCodex·GPT-5.5·10 steps
Changes
2
cmd/entire/cli
Msession_adopt.go +18/-11
Msession_adopt_test.go +75
60 unmodified lines
return errors.New("source worktree is required; pass --from <path>")
sourceStore, sourceWorktree, err := stateStoreForWorktree(ctx, opts.FromWorktree)
sourceStore, sourceWorktree, sourceCommonDir, err := stateStoreForWorktree(ctx, opts.FromWorktree)
if err != nil {
return err
}
targetStore, _, targetCommonDir, err := stateStoreForWorktree(ctx, ".")
if err != nil {
return fmt.Errorf("open current session store: %w", err)
}
if sourceCommonDir == targetCommonDir {
return errors.New("source and target share the same git common dir; session adopt only moves sessions across independent git session stores")
}
sourceState, err := selectAdoptSourceSession(ctx, sourceStore, sourceWorktree, sessionID)
if err != nil {
return err
}
return err
}
```go
targetStore, err := session.NewStateStore(ctx)
if err != nil {
return fmt.Errorf("open current session store: %w", err)
}
existing, err := targetStore.Load(ctx, adopted.SessionID)
if err != nil {
return fmt.Errorf("load current session state: %w", err)
}
return nil
}
func stateStoreForWorktree(ctx context.Context, worktreePath string) (*session.StateStore, string, error) {
func stateStoreForWorktree(ctx context.Context, worktreePath string) (*session.StateStore, string, string, error) {
absWorktree, err := filepath.Abs(worktreePath)
if err != nil {
return nil, "", fmt.Errorf("resolve source worktree: %w", err)
return nil, "", "", fmt.Errorf("resolve source worktree: %w", err)
}
cmd := exec.CommandContext(ctx, "git", "-C", absWorktree, "rev-parse", "--show-toplevel", "--git-common-dir")
if err != nil {
msg := strings.TrimSpace(string(output))
if msg != "" {
return nil, "", fmt.Errorf("resolve source git directory: %s: %w", msg, err)
return nil, "", "", fmt.Errorf("resolve source git directory: %s: %w", msg, err)
}
return nil, "", fmt.Errorf("resolve source git directory: %w", err)
return nil, "", "", fmt.Errorf("resolve source git directory: %w", err)
}
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
if len(lines) < 2 {
return nil, "", fmt.Errorf("resolve source git directory: unexpected git output %q", strings.TrimSpace(string(output)))
return nil, "", "", fmt.Errorf("resolve source git directory: unexpected git output %q", strings.TrimSpace(string(output)))
}
sourceRoot := strings.TrimSpace(lines[0])
commonDir := strings.TrimSpace(lines[1])
commonDir = filepath.Join(absWorktree, commonDir)
}
commonDir = filepath.Clean(commonDir)
if resolved, err := filepath.EvalSymlinks(commonDir); err == nil {
commonDir = resolved
}
return session.NewStateStoreWithDir(filepath.Join(commonDir, session.SessionStateDirName)), sourceRoot, nil
return session.NewStateStoreWithDir(filepath.Join(commonDir, session.SessionStateDirName)), sourceRoot, commonDir, nil
}
func selectAdoptSourceSession(ctx context.Context, store *session.StateStore, sourceWorktree, sessionID string) (*session.State, error) {
return err
}
3 unmodified lines
func TestSessionAdopt_RejectsSameGitCommonDir(t *testing.T) {
sourceRepo := setupAdoptRepo(t)
targetWorktree := filepath.Join(t.TempDir(), "target-worktree")
runAdoptGit(t, sourceRepo, "worktree", "add", targetWorktree, "-b", "target-worktree")
t.Cleanup(func() {
runAdoptGit(t, sourceRepo, "worktree", "remove", targetWorktree, "--force")
})
sessionID := "test-adopt-same-common-dir"
lastInteraction := time.Now().Add(-1 * time.Minute)
sourceStore := session.NewStateStoreWithDir(filepath.Join(sourceRepo, ".git", session.SessionStateDirName))
if err := sourceStore.Save(context.Background(), &session.State{
SessionID: sessionID,
AgentType: agent.AgentTypeClaudeCode,
StartedAt: time.Now().Add(-5 * time.Minute),
LastInteractionTime: &lastInteraction,
Phase: session.PhaseActive,
BaseCommit: testutil.GetHeadHash(t, sourceRepo),
WorktreePath: sourceRepo,
StepCount: 4,
CheckpointTranscriptStart: 2,
LastCheckpointID: id.MustCheckpointID("abc123def456"),
LastCheckpointCommitHash: "source-commit",
}); err != nil {
t.Fatal(err)
}
testutil.WriteFile(t, targetWorktree, "feature.txt", "agent change\n")
t.Chdir(targetWorktree)
var out bytes.Buffer
err := runAdopt(context.Background(), &out, sessionID, adoptOptions{
FromWorktree: sourceRepo,
Force: true,
})
if err == nil {
t.Fatal("runAdopt succeeded, want same-common-dir refusal")
}
if !strings.Contains(err.Error(), "same git common dir") {
t.Fatalf("runAdopt error = %v, want same git common dir refusal", err)
}
loaded, err := sourceStore.Load(context.Background(), sessionID)
if err != nil {
t.Fatal(err)
}
if loaded == nil {
t.Fatal("expected source session state to remain")
}
if loaded.StepCount != 4 {
t.Fatalf("StepCount = %d, want source state preserved at 4", loaded.StepCount)
}
if loaded.CheckpointTranscriptStart != 2 {
t.Fatalf("CheckpointTranscriptStart = %d, want source state preserved at 2", loaded.CheckpointTranscriptStart)
}
if loaded.LastCheckpointID.String() != "abc123def456" {
t.Fatalf("LastCheckpointID = %s, want source checkpoint preserved", loaded.LastCheckpointID.String())
}
if loaded.LastCheckpointCommitHash != "source-commit" {
t.Fatalf("LastCheckpointCommitHash = %q, want source commit preserved", loaded.LastCheckpointCommitHash)
}
}
func setupAdoptRepo(t *testing.T) string {
t.Helper()
return realRepoDir
}
func runAdoptGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.CommandContext(context.Background(), "git", args...)
cmd.Dir = dir
cmd.Env = testutil.GitIsolatedEnv()
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %s failed: %v\n%s", strings.Join(args, " "), err, output)
}
}