fix(enable): make import offer cancellable and fully best-effort · Entire

fix(enable): make import offer cancellable and fully best-effort

87360f3→main·

computermode·2w ago·4 files·+53 added/-42 removed

Address PR review feedback:
- Thread ctx into promptImportSelection and use form.RunWithContext so parent-command cancellation stops the prompt and is handled by handleFormCancellation.
- Never fail enable on a prompt/UI error: maybeOfferSessionImport is now void; prompt failures are logged, surfaced as a note, and skipped.
- Fix stale doc comment referring to the old claudeFixture name.

Sessions

e83ecb55af58View transcript

?\
Import Agent History on First-Time EnableClaude Code·Opus 4.8·3 steps

Changes

4

10 unmodified lines

11
12
13
14
14
15
16
17

10 unmodified lines

"github.com/stretchr/testify/require"
// claudeFixture is a two-turn Claude transcript used to verify enable-time import.
// claudeImportFixture is a two-turn Claude transcript used to verify enable-time import.
const claudeImportFixture = `{"type":"user","uuid":"u1","timestamp":"2026-06-20T00:00:00Z","message":{"role":"user","content":"first"}}\n{"type":"assistant","uuid":"a1","message":{"id":"m1","model":"claude-x","content":[{"type":"text","text":"ok"}],"usage":{"output_tokens":5}}}\n{"type":"user","uuid":"u2","timestamp":"2026-06-20T00:01:00Z","message":{"role":"user","content":"second"}}`

Mcmd/entire/cli/integration_test/enable_import_test.go+1/-1

1207 unmodified lines

1208
1209
1210
1211
1212
1213
1211
1212
1213
1214
486 unmodified lines

1701
1702
1703
1706
1707
1708
1704
1705
1706
1707

1207 unmodified lines

// Offer to import pre-existing agent history for the just-selected agents.
    // First-run only; best-effort (never fails enable).
    if err := maybeOfferSessionImport(ctx, w, agents, opts, firstRun); err != nil {
        return err
    }
    maybeOfferSessionImport(ctx, w, agents, opts, firstRun)

if opts.SuppressDoneMessage {
        // Bootstrap finalize will print its own completion summary after
        486 unmodified lines

// Offer to import pre-existing history for the just-configured agent.
    // First-run only; best-effort (never fails enable).
    if err := maybeOfferSessionImport(ctx, w, []agent.Agent{ag}, opts, firstRun); err != nil {
        return err
    }
    maybeOfferSessionImport(ctx, w, []agent.Agent{ag}, opts, firstRun)

if opts.SuppressDoneMessage {
        // Bootstrap finalize will print its own completion summary.
    }

Mcmd/entire/cli/setup.go+2/-6

41 unmodified lines

42
43
44
45
45
46
47
47
48
49
50
51
52
53
54
54
55
56
57
58
59
59
60
61
62
63
64
64
65
66
66
67
68
69
70
71
72
73
70
74
75
76
77
74
78
79
80
38 unmodified lines

119
120
121
119
122
123
124
125
12 unmodified lines

138
139
140
138
139
141
142
143
144
145
146

41 unmodified lines

// Interactive runs present a multi-select with nothing pre-checked, so import
// only happens when the user actively selects agents. Non-interactive runs
// (`--yes` or no TTY) auto-import all eligible agents.
func maybeOfferSessionImport(ctx context.Context, w io.Writer, agents []agent.Agent, opts EnableOptions, firstRun bool) error {
func maybeOfferSessionImport(ctx context.Context, w io.Writer, agents []agent.Agent, opts EnableOptions, firstRun bool) {
    if !firstRun {
        return nil
    }

repoRoot, err := paths.WorktreeRoot(ctx)
    if err != nil {
        // No worktree root => nothing to import against. Enabling still succeeds.
        logging.Warn(ctx, "session import offer skipped: no worktree root", "error", err)
        return nil
    }

eligible := sessionImportDiscover(ctx, agents, repoRoot)
    if len(eligible) == 0 {
        return nil
    }

selected := eligible
    if !opts.Yes && interactive.CanPromptInteractively() {
        selected, err = sessionImportPrompt(w, eligible)
        selected, err = sessionImportPrompt(ctx, w, eligible)
        if err != nil {
            return err
            // Best-effort: a prompt/UI failure must never fail enable. Log,
            // note it, and skip import.
            logging.Warn(ctx, "session import offer skipped: prompt failed", "error", err)
            fmt.Fprintf(w, "Note: could not show import prompt: %v\n", err)
            return
        }
    }
    if len(selected) == 0 {
        return nil
    }

sessionImportRun(ctx, w, repoRoot, selected)
    return nil
}

// discoverImportableAgents keeps the selected agents that have a registered
38 unmodified lines

// promptImportSelection shows the agent multi-select (all unchecked) and
// returns the chosen subset. An empty selection (or user abort) returns an
// empty slice, which the caller treats as "skip import".
func promptImportSelection(w io.Writer, eligible []eligibleImport) ([]eligibleImport, error) {
func promptImportSelection(ctx context.Context, w io.Writer, eligible []eligibleImport) ([]eligibleImport, error) {
    byName := make(map[string]eligibleImport, len(eligible))
    options := make([]huh.Option[string], 0, len(eligible))
    for _, e := range eligible {
12 unmodified lines

Value(&chosen),
        ),
    )
    if err := form.Run(); err != nil {
        // Cancellation returns nil here => skip import; other errors propagate.
    if err := form.RunWithContext(ctx); err != nil {
        // Cancellation (including a cancelled ctx) returns nil here => skip
        // import; other errors are surfaced for the caller to downgrade.
        return nil, handleFormCancellation(w, "Import", err)
    }
}

Mcmd/entire/cli/setup_import.go+15/-11

1 unmodified line

2
3
4
5
6
7
8
50 unmodified lines

59
60
61
61
62
63
64
65
20 unmodified lines

86
87
88
88
89
90
91
89
90
91
92
13 unmodified lines

106
107
108
111
109
110
111
112
1 unmodified line

114
115
116
119
120
121
122
117
118
119
120
14 unmodified lines

135
136
137
143
144
145
146
138
139
140
141
13 unmodified lines

155
156
157
166
158
159
160
161
162
163
172
173
174
175
164
165
166
167
10 unmodified lines

178
179
180
192
181
182
183
184
196
197
198
199
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214

1 unmodified line

import (
    "context"
    "errors"
    "io"
    "testing"

50 unmodified lines

// withImportSeams overrides the package seams and restores them after the test.
// Tests using it must not call t.Parallel (shared package state).
func withImportSeams(t *testing.T, discover func(context.Context, []agent.Agent, string) []eligibleImport, prompt func(io.Writer, []eligibleImport) ([]eligibleImport, error), run func(context.Context, io.Writer, string, []eligibleImport)) {
func withImportSeams(t *testing.T, discover func(context.Context, []agent.Agent, string) []eligibleImport, prompt func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error), run func(context.Context, io.Writer, string, []eligibleImport)) {
    t.Helper()
    oldDiscover, oldPrompt, oldRun := sessionImportDiscover, sessionImportPrompt, sessionImportRun
    t.Cleanup(func() {
20 unmodified lines

return []eligibleImport{{displayName: "X", sessionCount: 1}}
    }, nil, nil)

err := maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, false /* firstRun */)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, false /* firstRun */)
    if called {
         t.Error("discovery ran on a non-first-run enable; the offer must be gated to first run")
    }
13 unmodified lines

promptCalled := false
    withImportSeams(t,
        func(context.Context, []agent.Agent, string) []eligibleImport { return eligible },
        func(io.Writer, []eligibleImport) ([]eligibleImport, error) {
        func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error) {
            promptCalled = true
            return nil, nil
        },
        )

// opts.Yes forces the non-interactive path even if a TTY is present.
    err := maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{Yes: true}, true)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{Yes: true}, true)
    if promptCalled {
        t.Error("prompt shown under --yes; non-interactive enable must not prompt")
    }
14 unmodified lines

func(context.Context, io.Writer, string, []eligibleImport) { runCalled = true },
    )

err := maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{Yes: true}, true)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{Yes: true}, true)
    if runCalled {
        t.Error("import ran with nothing discoverable; expected a silent no-op")
    }
13 unmodified lines

var ran []eligibleImport
    withImportSeams(t,
        func(context.Context, []agent.Agent, string) []eligibleImport { return eligible },
        func(_ io.Writer, e []eligibleImport) ([]eligibleImport, error) {
        func(_ context.Context, _ io.Writer, e []eligibleImport) ([]eligibleImport, error) {
            return e[:1], nil // user picks only the first
        },
        func(_ context.Context, _ io.Writer, _ string, sel []eligibleImport) { ran = sel },
    )

err := maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, true)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, true)
    if len(ran) != 1 || ran[0].displayName != testAgentClaude {
        t.Fatalf("imported %+v, want only the user-selected Claude Code", ran)
    }
10 unmodified lines

func(context.Context, []agent.Agent, string) []eligibleImport {
            return []eligibleImport{{displayName: testAgentClaude, sessionCount: 3}}
        },
        func(io.Writer, []eligibleImport) ([]eligibleImport, error) { return nil, nil },
        func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error) { return nil, nil },
        func(context.Context, io.Writer, string, []eligibleImport) { runCalled = true },
    )

err := maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, true)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, true)
    if runCalled {
        t.Error("import ran after an empty selection; expected skip")
    }
}

func TestMaybeOfferSessionImport_PromptErrorIsBestEffort(t *testing.T) {
    dir := t.TempDir()
    testutil.InitRepo(t, dir)
    t.Chdir(dir)
    t.Setenv("ENTIRE_TEST_TTY", "1")

runCalled := false
    withImportSeams(t,
        func(context.Context, []agent.Agent, string) []eligibleImport {
            return []eligibleImport{{displayName: testAgentClaude, sessionCount: 3}}
        },
        func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error) {
            return nil, errors.New("terminal exploded")
        },
        func(context.Context, io.Writer, string, []eligibleImport) { runCalled = true },
    )

// A prompt failure must never fail enable: the offer is best-effort, so this
    // simply returns and does not panic or propagate.
    maybeOfferSessionImport(context.Background(), io.Discard, nil, EnableOptions{}, true)
    if runCalled {
        t.Error("import ran after a prompt error; expected skip")
    }
}