cli/enable: gate import offer on checkpoint policy; don't auto-import non-interactively · Entire

cli/enable: gate import offer on checkpoint policy; don't auto-import non-interactively

Two fixes from PR review:

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Sessions

01KWWDN6KCZAM8GGPP06D8A1HDView transcript

Changes

2

38 unmodified lines

// lookback, matching `entire import`). It is best-effort — discovery or import
// failures are logged and reported to the user but never fail enable.
//
// 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.
// Import only happens on an explicit choice: an interactive run presents a
// multi-select (nothing pre-checked) and imports what the user selects; `--yes`
// ("accept all defaults") auto-imports all eligible agents. A non-interactive
// run without `--yes` (a script, a piped shell, or an agent with no TTY) makes
// no choice, so it imports nothing and just points at `entire import` — silently
// importing history there would be surprising.
func maybeOfferSessionImport(ctx context.Context, w io.Writer, agents []agent.Agent, opts EnableOptions, firstRun bool) {
    if !firstRun {
        return
    }

selected := eligible
    if !opts.Yes && interactive.CanPromptInteractively() {
        if !opts.Yes {
            if !interactive.CanPromptInteractively() {
                // Non-interactive without --yes: don't silently import. Leave a
                // pointer so scripted/agent enables can still import on demand.
                logging.Info(ctx, "session import offer skipped: non-interactive without --yes", "eligible", len(eligible))
                fmt.Fprintf(w, "Found importable history for %s. Run 'entire import <agent>' to import it.\n", pluralAgents(len(eligible)))
                return
            }
            selected, err = sessionImportPrompt(ctx, w, eligible)
            if err != nil {
                // Best-effort: a prompt/UI failure must never fail enable. Log,
            }
        }
    }

// Gate on the checkpoint policy before writing any checkpoint data, matching
    // the standalone `entire import` command. Best-effort: an unsupported or
    // unreadable policy skips the import (logged and noted) instead of failing
    // enable, since the offer must never break enable.
    if err := ensureCheckpointPolicyAllowsCheckpointData(ctx, repo); err != nil {
        logging.Warn(ctx, "session import skipped: checkpoint policy not satisfied", "error", err)
        fmt.Fprintf(w, "Note: skipping agent history import: %v\n", err)
        return
    }

// Load repo/user-configured redaction before any checkpoint write, matching
    // import_cmd.go; without it only always-on secret scanning would run.
    strategy.EnsureRedactionConfigured()
}
// pluralAgents renders an agent count with correct pluralization.
func pluralAgents(n int) string {
    if n == 1 {
        return "1 agent"
    }
    return fmt.Sprintf("%d agents", n)
}
func TestMaybeOfferSessionImport_NonInteractiveWithoutYesSkips(t *testing.T) {
    // Not parallel: overrides seams and chdirs into a temp repo.
    dir := t.TempDir()
    testutil.InitRepo(t, dir)
    t.Chdir(dir)
    // No ENTIRE_TEST_TTY => CanPromptInteractively() is false (non-interactive),
    // e.g. a scripted or agent-driven enable.

promptCalled := false
    var ran []eligibleImport
    withImportSeams(t,
        func(context.Context, []agent.Agent, string) []eligibleImport {
            return []eligibleImport{{displayName: testAgentClaude, sessionCount: 3}}
        },
        func(context.Context, io.Writer, []eligibleImport) ([]eligibleImport, error) {
            promptCalled = true
            return nil, nil
        },
        func(_ context.Context, _ io.Writer, _ string, sel []eligibleImport) { ran = sel },
    )

// No --yes and no TTY: neither prompt nor auto-import; just hint at the
    // manual command.
    var buf bytes.Buffer
    maybeOfferSessionImport(context.Background(), &buf, nil, EnableOptions{}, true)
    if promptCalled {
        t.Error("prompt shown in a non-interactive context")
    }
    if len(ran) != 0 {
        t.Errorf("auto-imported %d agent(s) without --yes in a non-interactive context; expected skip", len(ran))
    }
    if got := buf.String(); !strings.Contains(got, "entire import") {
        t.Errorf("expected a pointer to 'entire import', got %q", got)
    }
}
func TestMaybeOfferSessionImport_NoEligibleIsNoOp(t *testing.T) {
    dir := t.TempDir()
    testutil.InitRepo(t, dir)
}
func TestRunSelectedImports_UnsatisfiablePolicySkips(t *testing.T) {
    dir := t.TempDir()
    testutil.InitRepo(t, dir)
    t.Chdir(dir)
    ctx := context.Background();

// Install a checkpoint policy this CLI cannot satisfy (a future format).
    // The gate must skip the import, matching the standalone `entire import`
    // command's ensureCheckpointPolicyAllowsCheckpointData check.
    repo, err := openRepository(ctx)
    if err != nil {
        t.Fatalf("open repository: %v", err)
    }
    future := checkpointpolicy.Policy{CheckpointVersion: "branch-v99", CheckpointMinVersion: "branch-v99"};
    if _, err := checkpointpolicy.WriteLocal(ctx, repo, plumbing.ZeroHash, future); err != nil {
        t.Fatalf("write local policy: %v", err)
    }
    repo.Close();

var buf bytes.Buffer
    runSelectedImports(ctx, &buf, dir, []eligibleImport{{displayName: testAgentClaude}})

if got := buf.String(); !strings.Contains(got, "skipping agent history import") {
        t.Errorf("expected a skip note for an unsatisfiable checkpoint policy, got %q", got)
    }
}
func TestMaybeOfferSessionImport_PromptErrorIsBestEffort(t *testing.T) {
    dir := t.TempDir()
    testutil.InitRepo(t, dir)
}