From 282f619e8cc70999fa4865d7a9fe1a88ea4c1a74 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 18 Jun 2026 14:05:04 -0400 Subject: [PATCH 1/3] fix(init): clarify secrets fallback wording Closes #340 --- .../cmd/credentialcmd/credentialcmd_test.go | 71 ++++++++++++++++++- .../credentialcmd/init_secrets_management.go | 37 +++++++--- .../init_secrets_management_editor.go | 21 +++++- 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/internal/cmd/credentialcmd/credentialcmd_test.go b/internal/cmd/credentialcmd/credentialcmd_test.go index 3c52d2aa..abf0e1ed 100644 --- a/internal/cmd/credentialcmd/credentialcmd_test.go +++ b/internal/cmd/credentialcmd/credentialcmd_test.go @@ -9866,8 +9866,11 @@ func TestHuhInitKeyringBackendPrompterAccessibleShowsField(t *testing.T) { if len(rows) == 0 { t.Fatal("initSecretsManagementInventoryRows returned no rows") } - if rows[0].Title != "Default credential store (Automatic OS default)" { - t.Fatalf("first row title = %q, want default credential-store row first", rows[0].Title) + if rows[0].Title != "Fallback credential store: "+initAutomaticOSDefaultSecretsBackendLabel() { + t.Fatalf("first row title = %q, want fallback credential-store row first", rows[0].Title) + } + if !strings.Contains(rows[0].Description, "Legacy fallback credential store") { + t.Fatalf("first row description = %q, want legacy fallback wording", rows[0].Description) } var foundConfigure bool for _, row := range rows { @@ -9883,6 +9886,29 @@ func TestHuhInitKeyringBackendPrompterAccessibleShowsField(t *testing.T) { if len(options) == 0 { t.Fatal("initLegacySecretsBackendOptions returned no options") } + if options[0].Key != initAutomaticOSDefaultSecretsBackendLabel() { + t.Fatalf("first legacy backend option = %q, want platform-aware automatic OS default", options[0].Key) + } +} + +func TestInitAutomaticOSDefaultSecretsBackendLabelForGOOS(t *testing.T) { + tests := []struct { + goos string + want string + }{ + {goos: "darwin", want: "Automatic OS default (macOS Keychain)"}, + {goos: "windows", want: "Automatic OS default (Windows Credential Manager)"}, + {goos: "linux", want: "Automatic OS default (Linux Secret Service)"}, + {goos: "plan9", want: "Automatic OS default"}, + } + + for _, tt := range tests { + t.Run(tt.goos, func(t *testing.T) { + if got := initAutomaticOSDefaultSecretsBackendLabelForGOOS(tt.goos); got != tt.want { + t.Fatalf("initAutomaticOSDefaultSecretsBackendLabelForGOOS(%q) = %q, want %q", tt.goos, got, tt.want) + } + }) + } } func TestInitSecretsManagementInventoryRowsUsePreferredBackendOrder(t *testing.T) { @@ -9892,7 +9918,7 @@ func TestInitSecretsManagementInventoryRowsUsePreferredBackendOrder(t *testing.T titles = append(titles, row.Title) } assertContentOrder(t, strings.Join(titles, "\n"), - "Default credential store", + "Fallback credential store", "Configure new macos keychain profile", "Configure new pass password store profile", "Configure new encrypted file profile", @@ -10587,6 +10613,45 @@ func TestHuhInitKeyringBackendPrompterDefaultUsesLinearSecretsManagementFlow(t * } } +func TestInitSecretsManagementLinearEditorShowsLegacyFallbackWording(t *testing.T) { + cfg := config.File{Profiles: map[string]config.Profile{"default": basicProfile("default")}, DefaultProfile: "default"} + editor := initSecretsManagementLinearEditor(cfg) + model := newInitLinearEditorModel(editor, 180, 32) + out := model.layout.Content + for _, want := range []string{ + "Secrets-management target", + "Fallback credential store: " + initAutomaticOSDefaultSecretsBackendLabel(), + "Legacy fallback credential store", + "Compatibility path for profiles that do not choose a named secrets-management profile.", + "Fallback persistent backend", + initAutomaticOSDefaultSecretsBackendLabel(), + } { + if !strings.Contains(out, want) { + t.Fatalf("initial linear editor output missing %q:\n%s", want, out) + } + } + if strings.Contains(out, "Default credential store") || strings.Contains(out, "Legacy persistent backend") { + t.Fatalf("initial linear editor output contains redundant legacy copy:\n%s", out) + } + + model = selectInitLinearFieldValue(t, model, initSecretsManagementFieldLegacyBackend, string(credstore.BackendMemory)) + index := model.document.fieldIndexByID(initSecretsManagementFieldLegacyBackend) + if index < 0 { + t.Fatal("legacy backend field missing") + } + description := model.document[index].Description + for _, want := range []string{ + "Ephemeral", + "tests or CI", + "not normal local use", + "legacy fallback backend", + } { + if !strings.Contains(description, want) { + t.Fatalf("legacy backend description = %q, want %q", description, want) + } + } +} + func TestHuhInitKeyringBackendPrompterLinearCanDeleteConfiguredSecretsProfile(t *testing.T) { cfg := config.File{ Profiles: map[string]config.Profile{"default": basicProfile("default")}, diff --git a/internal/cmd/credentialcmd/init_secrets_management.go b/internal/cmd/credentialcmd/init_secrets_management.go index 711acce3..0905a872 100644 --- a/internal/cmd/credentialcmd/init_secrets_management.go +++ b/internal/cmd/credentialcmd/init_secrets_management.go @@ -109,7 +109,7 @@ func initSecretsBackendDescription(kind config.SecretsBackendKind) string { case config.SecretsBackendKind(credstore.BackendOPDesktop): return "Use the local 1Password desktop app integration. Most common for local use; best for interactive developer machines with an unlocked desktop app; requires a vault name or id and can optionally pin a desktop account id." case config.SecretsBackendKind(credstore.BackendMemory): - return "Keep credentials in memory only. Best suited for tests or CI." + return "Keep credentials in memory only for this process. Ephemeral; best suited for tests or CI, not normal local use." default: return "" } @@ -147,6 +147,23 @@ func initSecretsManagementInventoryDescription() string { return "Choose how cr should store credentials. Secrets-management profiles are reusable store definitions that review profiles can choose later." } +func initAutomaticOSDefaultSecretsBackendLabel() string { + return initAutomaticOSDefaultSecretsBackendLabelForGOOS(runtime.GOOS) +} + +func initAutomaticOSDefaultSecretsBackendLabelForGOOS(goos string) string { + switch goos { + case "darwin": + return "Automatic OS default (macOS Keychain)" + case "windows": + return "Automatic OS default (Windows Credential Manager)" + case "linux": + return "Automatic OS default (Linux Secret Service)" + default: + return "Automatic OS default" + } +} + func initSecretsManagementInventoryRows(cfg config.File) []initInventoryRow { effective := config.EffectiveSecretsProfiles(cfg) rows := make([]initInventoryRow, 0, len(effective)+len(initSecretsBackendCatalog())+2) @@ -166,11 +183,11 @@ func initSecretsManagementInventoryRows(cfg config.File) []initInventoryRow { rows = append(rows, initInventoryRow{ ID: initSecretsManagementLegacySelection, Title: initLegacySecretsManagementInventoryTitle(cfg), - Description: "Global fallback credential store used by profiles that do not choose a named secrets-management profile.", + Description: "Legacy fallback credential store used only by profiles that do not choose a named secrets-management profile.", Kind: initInventoryRowKindActive, PrimaryAction: initInventoryActionCommand, Selectable: true, - FilterValue: strings.TrimSpace(strings.Join([]string{"default credential store global fallback keyring backend", strings.TrimSpace(cfg.Keyring.Backend)}, " ")), + FilterValue: strings.TrimSpace(strings.Join([]string{"fallback compatibility legacy credential store keyring backend", strings.TrimSpace(cfg.Keyring.Backend)}, " ")), }) for _, backend := range initSecretsBackendCatalog() { @@ -223,9 +240,9 @@ func initLegacySecretsManagementInventoryTitle(cfg config.File) string { } } if backend == config.ProjectedLegacySecretsBackendKind { - backendLabel = "Automatic OS default" + backendLabel = initAutomaticOSDefaultSecretsBackendLabel() } - return fmt.Sprintf("Default credential store (%s)", backendLabel) + return fmt.Sprintf("Fallback credential store: %s", backendLabel) } func initSecretsProfileDisplayName(id string, label string) string { @@ -289,7 +306,7 @@ func initSecretsProfileBackendOptions(current config.SecretsBackendKind) []huh.O func initLegacySecretsBackendOptions(current string) []huh.Option[string] { options := []huh.Option[string]{ - huh.NewOption("Automatic OS default", ""), + huh.NewOption(initAutomaticOSDefaultSecretsBackendLabel(), ""), } for _, backend := range initSecretsBackendCatalog() { if !backend.LegacyCompatible { @@ -703,17 +720,17 @@ func (p huhInitKeyringBackendPrompter) editLegacySecretsManagement(currentBacken form := huh.NewForm( huh.NewGroup( huh.NewSelect[string](). - Title("Default credential store backend"). + Title("Legacy fallback credential store backend"). Options(initLegacySecretsBackendOptions(currentBackend)...). Value(&backend), huh.NewSelect[string](). - Title("Default credential-store action"). + Title("Fallback credential-store action"). Options( - huh.NewOption("Stage default credential-store settings", initDetailActionEdit), + huh.NewOption("Stage fallback credential-store settings", initDetailActionEdit), huh.NewOption("Back without staging", initDetailActionBack), ). Value(&action), - ).Title("Default Credential Store"), + ).Title("Legacy Fallback Credential Store"), ) back, err := runBackableInitForm(form, p.stdin, p.stderr) if err != nil { diff --git a/internal/cmd/credentialcmd/init_secrets_management_editor.go b/internal/cmd/credentialcmd/init_secrets_management_editor.go index 0ed98067..4afaeebe 100644 --- a/internal/cmd/credentialcmd/init_secrets_management_editor.go +++ b/internal/cmd/credentialcmd/init_secrets_management_editor.go @@ -134,8 +134,8 @@ func initSecretsManagementLinearEditorWithPendingOrder(cfg config.File, pendingD var document initLinearDocument document.addSection("Secrets management", initSecretsManagementInventoryDescription()) document.addEditableSelect(initSecretsManagementFieldTarget, "Secrets-management target", "", targetOptions, selectedTarget) - document.addSectionField(initSecretsManagementSectionLegacy, "Default credential store", "Global fallback credential store used by profiles that do not choose a named secrets-management profile.") - document.addEditableSelect(initSecretsManagementFieldLegacyBackend, "Legacy persistent backend", "", initLegacySecretsBackendOptions(cfg.Keyring.Backend), strings.TrimSpace(cfg.Keyring.Backend)) + document.addSectionField(initSecretsManagementSectionLegacy, "Legacy fallback credential store", "Compatibility path for profiles that do not choose a named secrets-management profile.") + document.addEditableSelect(initSecretsManagementFieldLegacyBackend, "Fallback persistent backend", initLegacySecretsBackendFieldDescription(cfg.Keyring.Backend), initLegacySecretsBackendOptions(cfg.Keyring.Backend), strings.TrimSpace(cfg.Keyring.Backend)) document.addSectionField(initSecretsManagementSectionProfile, "Secrets-management profile", "Secrets-management profiles are reusable credential-store definitions that review profiles can choose later.") document.addEditableInput( initSecretsManagementFieldLabel, @@ -179,6 +179,10 @@ func initSecretsManagementLinearEditorWithPendingOrder(cfg config.File, pendingD } if id == initSecretsManagementFieldBackend { initSecretsManagementSyncLinearFields(model, cfg, pendingDeletes, pendingDeleteOrder, false) + return + } + if id == initSecretsManagementFieldLegacyBackend { + initSecretsManagementSyncLinearFields(model, cfg, pendingDeletes, pendingDeleteOrder, false) } }, OnEnter: func(model *initLinearEditorModel) (bool, tea.Cmd) { @@ -336,6 +340,7 @@ func initSecretsManagementSyncLinearFields(model *initLinearEditorModel, cfg con model.setFieldHidden(initSecretsManagementFieldDefault, !profileVisible) initSecretsManagementSetActionOptions(model, !state.Creating && !state.Legacy && !state.Pending) if state.Legacy { + model.setFieldDescription(initSecretsManagementFieldLegacyBackend, initLegacySecretsBackendFieldDescription(model.document.selectedValue(initSecretsManagementFieldLegacyBackend))) initSecretsManagementSetOnePasswordHidden(model, true, true, true, true) return } @@ -501,6 +506,18 @@ func initSecretsManagementBackendFieldDescription(kind config.SecretsBackendKind return strings.TrimSpace(description + " Use Up/Down here to change the backend for this configured secrets-management profile.") } +func initLegacySecretsBackendFieldDescription(backend string) string { + trimmed := strings.TrimSpace(backend) + if trimmed == "" { + return "Use the platform credential store chosen by the OS, such as macOS Keychain on Darwin, Windows Credential Manager on Windows, or Linux Secret Service on Linux." + } + description := strings.TrimSpace(initSecretsBackendDescription(config.SecretsBackendKind(trimmed))) + if description == "" { + return "Compatibility backend used only by profiles that do not choose a named secrets-management profile." + } + return strings.TrimSpace(description + " This is the legacy fallback backend for profiles without a named secrets-management profile.") +} + func initSecretsManagementSelectedOptionLabel(document initLinearDocument, id initLinearFieldID) string { index := document.fieldIndexByID(id) if index < 0 { From a4a6a66c8aeca802e2e5d001fc412639b898f3e7 Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 18 Jun 2026 14:08:51 -0400 Subject: [PATCH 2/3] test(init): cover fallback secrets form copy --- .../cmd/credentialcmd/credentialcmd_test.go | 43 +++++++++++++++++++ .../init_secrets_management_editor.go | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/internal/cmd/credentialcmd/credentialcmd_test.go b/internal/cmd/credentialcmd/credentialcmd_test.go index abf0e1ed..ec102ca2 100644 --- a/internal/cmd/credentialcmd/credentialcmd_test.go +++ b/internal/cmd/credentialcmd/credentialcmd_test.go @@ -10555,6 +10555,49 @@ func TestHuhInitKeyringBackendPrompterStagesNewSecretsProfileEndToEnd(t *testing } } +func TestHuhInitKeyringBackendPrompterAccessibleLegacyFallbackFormShowsFallbackCopy(t *testing.T) { + t.Setenv("TERM", "dumb") + var stderr bytes.Buffer + prompter := huhInitKeyringBackendPrompter{ + stdin: strings.NewReader(strings.Join([]string{ + "", // keep selected memory backend + "", // stage fallback settings + "", + }, "\n")), + stderr: &stderr, + } + + edit, err := prompter.editLegacySecretsManagement(string(credstore.BackendMemory)) + if err != nil { + t.Fatalf("editLegacySecretsManagement: %v", err) + } + if !edit.Apply || edit.Backend != string(credstore.BackendMemory) { + t.Fatalf("edit = %#v, want staged memory fallback backend", edit) + } + out := stderr.String() + for _, want := range []string{ + "Legacy fallback credential store backend", + "Fallback credential-store action", + "Stage fallback credential-store settings", + initAutomaticOSDefaultSecretsBackendLabel(), + "In-memory store", + } { + if !strings.Contains(out, want) { + t.Fatalf("legacy fallback form output missing %q:\n%s", want, out) + } + } + for _, stale := range []string{ + "Default Credential Store", + "Default credential store backend", + "Default credential-store action", + "Stage default credential-store settings", + } { + if strings.Contains(out, stale) { + t.Fatalf("legacy fallback form output contains stale copy %q:\n%s", stale, out) + } + } +} + func TestHuhInitKeyringBackendPrompterDefaultUsesLinearSecretsManagementFlow(t *testing.T) { var stderr bytes.Buffer prompter := huhInitKeyringBackendPrompter{ diff --git a/internal/cmd/credentialcmd/init_secrets_management_editor.go b/internal/cmd/credentialcmd/init_secrets_management_editor.go index 4afaeebe..d4bce630 100644 --- a/internal/cmd/credentialcmd/init_secrets_management_editor.go +++ b/internal/cmd/credentialcmd/init_secrets_management_editor.go @@ -509,7 +509,7 @@ func initSecretsManagementBackendFieldDescription(kind config.SecretsBackendKind func initLegacySecretsBackendFieldDescription(backend string) string { trimmed := strings.TrimSpace(backend) if trimmed == "" { - return "Use the platform credential store chosen by the OS, such as macOS Keychain on Darwin, Windows Credential Manager on Windows, or Linux Secret Service on Linux." + return "Use the platform credential store chosen by the OS, such as macOS Keychain on macOS, Windows Credential Manager on Windows, or Linux Secret Service on Linux." } description := strings.TrimSpace(initSecretsBackendDescription(config.SecretsBackendKind(trimmed))) if description == "" { From a6b2177bab849f218619e712c6c6ff317e2afb4c Mon Sep 17 00:00:00 2001 From: Rian Stockbower Date: Thu, 18 Jun 2026 14:13:22 -0400 Subject: [PATCH 3/3] test(init): cover legacy fallback secrets flow --- .../cmd/credentialcmd/credentialcmd_test.go | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/internal/cmd/credentialcmd/credentialcmd_test.go b/internal/cmd/credentialcmd/credentialcmd_test.go index ec102ca2..c6bcca0f 100644 --- a/internal/cmd/credentialcmd/credentialcmd_test.go +++ b/internal/cmd/credentialcmd/credentialcmd_test.go @@ -10598,6 +10598,71 @@ func TestHuhInitKeyringBackendPrompterAccessibleLegacyFallbackFormShowsFallbackC } } +func TestHuhInitKeyringBackendPrompterInventoryLegacyFallbackStagesBackend(t *testing.T) { + t.Setenv("TERM", "dumb") + memoryChoice := 0 + for index, option := range initLegacySecretsBackendOptions("") { + if option.Value == string(credstore.BackendMemory) { + memoryChoice = index + 1 + break + } + } + if memoryChoice == 0 { + t.Fatal("memory backend option missing from legacy fallback backend options") + } + var stderr bytes.Buffer + callCount := 0 + prompter := huhInitKeyringBackendPrompter{ + stdin: strings.NewReader(fmt.Sprintf("%d\n1\n", memoryChoice)), + stderr: &stderr, + inventoryRunner: func(_ initInventoryPrompt, _ io.Reader, _ io.Writer) (initInventoryResult, error) { + callCount++ + switch callCount { + case 1: + return initInventoryResult{ + Action: initInventoryActionCommand, + Row: initInventoryRow{ + ID: initSecretsManagementLegacySelection, + Title: initLegacySecretsManagementInventoryTitle(config.File{}), + }, + }, nil + case 2: + return initInventoryResult{ + Action: initInventoryActionBack, + Row: initInventoryRow{ID: initBackSelection}, + }, nil + default: + t.Fatalf("unexpected inventory call %d", callCount) + return initInventoryResult{}, nil + } + }, + } + + edit, err := prompter.EditKeyringBackend(initKeyringBackendPrompt{ + Config: config.File{Profiles: map[string]config.Profile{"default": basicProfile("default")}, DefaultProfile: "default"}, + }) + if err != nil { + t.Fatalf("EditKeyringBackend: %v", err) + } + if !edit.Apply || !edit.HasConfigEdit { + t.Fatalf("edit = %#v, want staged config edit", edit) + } + if got := edit.Config.Keyring.Backend; got != string(credstore.BackendMemory) { + t.Fatalf("keyring.backend = %q, want memory after legacy fallback inventory path", got) + } + out := stderr.String() + for _, want := range []string{ + "Legacy fallback credential store backend", + "Fallback credential-store action", + "Stage fallback credential-store settings", + "In-memory store", + } { + if !strings.Contains(out, want) { + t.Fatalf("legacy fallback inventory path output missing %q:\n%s", want, out) + } + } +} + func TestHuhInitKeyringBackendPrompterDefaultUsesLinearSecretsManagementFlow(t *testing.T) { var stderr bytes.Buffer prompter := huhInitKeyringBackendPrompter{ @@ -10693,6 +10758,15 @@ func TestInitSecretsManagementLinearEditorShowsLegacyFallbackWording(t *testing. t.Fatalf("legacy backend description = %q, want %q", description, want) } } + for _, want := range []string{ + "Keep credentials in memory only for this process.", + "Ephemeral; best suited for tests or CI, not normal local use.", + "[x] In-memory store", + } { + if !strings.Contains(model.layout.Content, want) { + t.Fatalf("memory-selected linear editor output missing %q:\n%s", want, model.layout.Content) + } + } } func TestHuhInitKeyringBackendPrompterLinearCanDeleteConfiguredSecretsProfile(t *testing.T) {