WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package profilecmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
|
||||
"github.com/Tencent/WeKnora/cli/internal/config"
|
||||
"github.com/Tencent/WeKnora/cli/internal/iostreams"
|
||||
"github.com/Tencent/WeKnora/cli/internal/secrets"
|
||||
"github.com/Tencent/WeKnora/cli/internal/testutil"
|
||||
)
|
||||
|
||||
// seedStore returns a MemStore pre-loaded with sentinel values for every
|
||||
// secret slot a profile might reference. Tests assert deletion by checking
|
||||
// `secrets.ErrNotFound` post-runRemove.
|
||||
func seedStore(t *testing.T, name string, slots ...string) *secrets.MemStore {
|
||||
t.Helper()
|
||||
s := secrets.NewMemStore()
|
||||
for _, slot := range slots {
|
||||
if err := s.Set(name, slot, "sentinel-"+name+"-"+slot); err != nil {
|
||||
t.Fatalf("seed %s/%s: %v", name, slot, err)
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func assertDeleted(t *testing.T, s *secrets.MemStore, name, slot string) {
|
||||
t.Helper()
|
||||
if _, err := s.Get(name, slot); !errors.Is(err, secrets.ErrNotFound) {
|
||||
t.Errorf("expected %s/%s removed, got err=%v", name, slot, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove_NonCurrent_NoPromptNeeded(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
out, _ := iostreams.SetForTest(t)
|
||||
|
||||
cfg := &config.Config{
|
||||
CurrentProfile: "production",
|
||||
Profiles: map[string]config.Profile{
|
||||
"production": {Host: "https://prod.example.com", TokenRef: "mem://production/access"},
|
||||
"staging": {Host: "https://staging.example.com", APIKeyRef: "mem://staging/api_key"},
|
||||
},
|
||||
}
|
||||
if err := config.Save(cfg); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
store := seedStore(t, "staging", "api_key")
|
||||
p := &testutil.ConfirmPrompter{}
|
||||
if err := runRemove(&RemoveOptions{}, &cmdutil.FormatOptions{Mode: cmdutil.FormatText}, "staging", store, p); err != nil {
|
||||
t.Fatalf("runRemove: %v", err)
|
||||
}
|
||||
if p.Asked {
|
||||
t.Errorf("non-current remove must not prompt")
|
||||
}
|
||||
|
||||
got, _ := config.Load()
|
||||
if _, exists := got.Profiles["staging"]; exists {
|
||||
t.Errorf("staging should have been removed; Profiles=%v", got.Profiles)
|
||||
}
|
||||
if got.CurrentProfile != "production" {
|
||||
t.Errorf("CurrentProfile must be unchanged, got %q", got.CurrentProfile)
|
||||
}
|
||||
assertDeleted(t, store, "staging", "api_key")
|
||||
if !strings.Contains(out.String(), "staging") {
|
||||
t.Errorf("output should mention removed profile, got %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove_NotFound_WithDidYouMean(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
_, _ = iostreams.SetForTest(t)
|
||||
|
||||
cfg := &config.Config{Profiles: map[string]config.Profile{
|
||||
"production": {Host: "https://prod"},
|
||||
"staging": {Host: "https://staging"},
|
||||
}}
|
||||
if err := config.Save(cfg); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
err := runRemove(&RemoveOptions{}, &cmdutil.FormatOptions{Mode: cmdutil.FormatText}, "prodution", secrets.NewMemStore(), &testutil.ConfirmPrompter{})
|
||||
if err == nil {
|
||||
t.Fatal("expected not-found error")
|
||||
}
|
||||
cm, ok := err.(*cmdutil.Error)
|
||||
if !ok {
|
||||
t.Fatalf("expected *cmdutil.Error, got %T", err)
|
||||
}
|
||||
if cm.Code != cmdutil.CodeLocalProfileNotFound {
|
||||
t.Errorf("code=%q, want %q", cm.Code, cmdutil.CodeLocalProfileNotFound)
|
||||
}
|
||||
if !strings.Contains(cm.Hint, "production") {
|
||||
t.Errorf("hint should suggest 'production', got %q", cm.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove_Current_NonTTY_NoYes_RequiresConfirmation(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
_, _ = iostreams.SetForTest(t)
|
||||
|
||||
cfg := &config.Config{
|
||||
CurrentProfile: "production",
|
||||
Profiles: map[string]config.Profile{
|
||||
"production": {Host: "https://prod", TokenRef: "mem://production/access"},
|
||||
"staging": {Host: "https://staging"},
|
||||
},
|
||||
}
|
||||
if err := config.Save(cfg); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
store := seedStore(t, "production", "access")
|
||||
err := runRemove(&RemoveOptions{}, &cmdutil.FormatOptions{Mode: cmdutil.FormatText}, "production", store, &testutil.ConfirmPrompter{})
|
||||
if err == nil {
|
||||
t.Fatal("expected confirmation-required error")
|
||||
}
|
||||
cm, ok := err.(*cmdutil.Error)
|
||||
if !ok {
|
||||
t.Fatalf("expected *cmdutil.Error, got %T", err)
|
||||
}
|
||||
if cm.Code != cmdutil.CodeInputConfirmationRequired {
|
||||
t.Errorf("code=%q, want %q", cm.Code, cmdutil.CodeInputConfirmationRequired)
|
||||
}
|
||||
if cmdutil.ExitCode(err) != 10 {
|
||||
t.Errorf("expected exit-10, got %d", cmdutil.ExitCode(err))
|
||||
}
|
||||
// Must not have mutated config or keyring.
|
||||
if got, _ := config.Load(); got.CurrentProfile != "production" {
|
||||
t.Errorf("config mutated despite confirmation gate: CurrentProfile=%q", got.CurrentProfile)
|
||||
}
|
||||
if v, err := store.Get("production", "access"); err != nil || v == "" {
|
||||
t.Errorf("keyring touched before confirmation, get=%q err=%v", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove_Current_WithYes_ClearsCurrent(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
out, _ := iostreams.SetForTest(t)
|
||||
|
||||
cfg := &config.Config{
|
||||
CurrentProfile: "production",
|
||||
Profiles: map[string]config.Profile{
|
||||
"production": {Host: "https://prod", TokenRef: "mem://production/access"},
|
||||
"staging": {Host: "https://staging"},
|
||||
},
|
||||
}
|
||||
if err := config.Save(cfg); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
store := seedStore(t, "production", "access")
|
||||
if err := runRemove(&RemoveOptions{Yes: true}, &cmdutil.FormatOptions{Mode: cmdutil.FormatText}, "production", store, &testutil.ConfirmPrompter{}); err != nil {
|
||||
t.Fatalf("runRemove: %v", err)
|
||||
}
|
||||
got, _ := config.Load()
|
||||
if _, exists := got.Profiles["production"]; exists {
|
||||
t.Errorf("production should be removed")
|
||||
}
|
||||
if got.CurrentProfile != "" {
|
||||
t.Errorf("removing current must clear CurrentProfile, got %q", got.CurrentProfile)
|
||||
}
|
||||
assertDeleted(t, store, "production", "access")
|
||||
if !strings.Contains(out.String(), "current profile cleared") {
|
||||
t.Errorf("output should warn about cleared current, got %q", out.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove_Current_TTY_PromptNo(t *testing.T) {
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
_, errBuf := iostreams.SetForTestWithTTY(t)
|
||||
|
||||
cfg := &config.Config{
|
||||
CurrentProfile: "production",
|
||||
Profiles: map[string]config.Profile{"production": {Host: "https://prod"}},
|
||||
}
|
||||
if err := config.Save(cfg); err != nil {
|
||||
t.Fatalf("Save: %v", err)
|
||||
}
|
||||
|
||||
p := &testutil.ConfirmPrompter{Answer: false}
|
||||
err := runRemove(&RemoveOptions{}, &cmdutil.FormatOptions{Mode: cmdutil.FormatText}, "production", secrets.NewMemStore(), p)
|
||||
if err == nil {
|
||||
t.Fatal("expected user-aborted error")
|
||||
}
|
||||
cm, ok := err.(*cmdutil.Error)
|
||||
if !ok {
|
||||
t.Fatalf("expected *cmdutil.Error, got %T", err)
|
||||
}
|
||||
if cm.Code != cmdutil.CodeUserAborted {
|
||||
t.Errorf("code=%q, want %q", cm.Code, cmdutil.CodeUserAborted)
|
||||
}
|
||||
if !p.Asked {
|
||||
t.Errorf("prompt should have been asked on TTY")
|
||||
}
|
||||
if !strings.Contains(errBuf.String(), "Aborted") {
|
||||
t.Errorf("stderr should contain Aborted, got %q", errBuf.String())
|
||||
}
|
||||
if got, _ := config.Load(); got.CurrentProfile != "production" {
|
||||
t.Errorf("aborted remove must not mutate config")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user