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

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)
}
}