From 76ee4cd05edb26c3e490de6ac58bb365723587bf Mon Sep 17 00:00:00 2001 From: luozhixiong Date: Wed, 15 Jul 2026 11:16:26 +0800 Subject: [PATCH] fix(config): pin saved-config commands to the saved default - config show now resolves the saved default profile unconditionally: its own help (and the skill routing) promise "saved config, not current usage", but the output still followed --profile / LARKSUITE_CLI_PROFILE. A behavior test locks the boundary the help text claims. - profile list dual-publishes the deprecated `active` field as an alias of `default` for one deprecation cycle, so external consumers reading `.active` keep working; it still marks the saved default, never the currently effective identity. --- cmd/config/config_test.go | 31 +++++++++++++++++++++++++++++++ cmd/config/show.go | 5 ++++- cmd/profile/list.go | 18 ++++++++++++------ cmd/profile/profile_test.go | 11 +++++++++-- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index 96b724454..3f6740338 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -116,6 +116,37 @@ func TestConfigShowRun_NotConfiguredReturnsStructuredError(t *testing.T) { } } +// config show promises "saved config, not current usage" (help + skill +// routing): the session profile (--profile / LARKSUITE_CLI_PROFILE) must not +// change what it shows. +func TestConfigShowRun_IgnoresSessionProfile(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + multi := &core.MultiAppConfig{ + CurrentApp: "tenant_a", + Apps: []core.AppConfig{ + {Name: "tenant_a", AppId: "cli_a", AppSecret: core.PlainSecret("s-a"), Brand: core.BrandFeishu}, + {Name: "tenant_b", AppId: "cli_b", AppSecret: core.PlainSecret("s-b"), Brand: core.BrandFeishu}, + }, + } + if err := core.SaveMultiAppConfig(multi); err != nil { + t.Fatalf("SaveMultiAppConfig: %v", err) + } + + f, stdout, _, _ := cmdutil.TestFactory(t, nil) + f.Invocation.Profile = "tenant_b" // session selection must not leak in + + if err := configShowRun(&ConfigShowOptions{Factory: f}); err != nil { + t.Fatalf("configShowRun: %v", err) + } + out := stdout.String() + if !strings.Contains(out, `"cli_a"`) || !strings.Contains(out, `"tenant_a"`) { + t.Fatalf("output = %s, want the saved default tenant_a/cli_a", out) + } + if strings.Contains(out, `"cli_b"`) { + t.Fatalf("output = %s, session profile tenant_b must not change saved-config view", out) + } +} + func TestConfigShowRun_NoActiveProfileReturnsStructuredError(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) multi := &core.MultiAppConfig{ diff --git a/cmd/config/show.go b/cmd/config/show.go index 9e3db054c..3a1e71974 100644 --- a/cmd/config/show.go +++ b/cmd/config/show.go @@ -54,7 +54,10 @@ func configShowRun(opts *ConfigShowOptions) error { if config == nil || len(config.Apps) == 0 { return core.NotConfiguredError() } - app := config.CurrentAppConfig(f.Invocation.Profile) + // Saved config only: the session profile (--profile / LARKSUITE_CLI_PROFILE) + // must not change what this command shows — the help and skill routing + // promise "saved config, not current usage" (use whoami for that). + app := config.CurrentAppConfig("") if app == nil { return errs.NewConfigError(errs.SubtypeNotConfigured, "no active profile").WithHint("run: lark-cli profile list") } diff --git a/cmd/profile/list.go b/cmd/profile/list.go index df4d607c4..ad1cd3890 100644 --- a/cmd/profile/list.go +++ b/cmd/profile/list.go @@ -18,12 +18,17 @@ import ( // profileListItem is the JSON output for a single profile entry. type profileListItem struct { - Name string `json:"name"` - AppID string `json:"appId"` - Brand core.LarkBrand `json:"brand"` - Default bool `json:"default"` - User string `json:"user,omitempty"` - TokenStatus string `json:"tokenStatus,omitempty"` + Name string `json:"name"` + AppID string `json:"appId"` + Brand core.LarkBrand `json:"brand"` + Default bool `json:"default"` + // Active is a deprecated alias of Default, dual-published for one + // deprecation cycle so external consumers reading `.active` keep + // working. It marks the saved default profile — never the currently + // effective identity (use whoami for that). Remove in the next major. + Active bool `json:"active"` + User string `json:"user,omitempty"` + TokenStatus string `json:"tokenStatus,omitempty"` } // NewCmdProfileList creates the profile list subcommand. @@ -71,6 +76,7 @@ func profileListRun(f *cmdutil.Factory) error { AppID: app.AppId, Brand: app.Brand, Default: name == currentName, + Active: name == currentName, } if len(app.Users) > 0 { diff --git a/cmd/profile/profile_test.go b/cmd/profile/profile_test.go index 3291cccb1..c93b2c9e1 100644 --- a/cmd/profile/profile_test.go +++ b/cmd/profile/profile_test.go @@ -307,8 +307,10 @@ func TestProfileListRun_OutputsProfiles(t *testing.T) { t.Fatalf("Unmarshal() error = %v; output=%s", err, stdout.String()) } raw := stdout.String() - if strings.Contains(raw, `"active"`) { - t.Fatalf("profile list output contains legacy active field: %s", raw) + // `active` is dual-published as a deprecated alias of `default` for one + // deprecation cycle so external consumers reading `.active` keep working. + if !strings.Contains(raw, `"active"`) { + t.Fatalf("profile list output missing deprecated active alias: %s", raw) } if !strings.Contains(raw, `"default"`) { t.Fatalf("profile list output missing default field: %s", raw) @@ -322,6 +324,11 @@ func TestProfileListRun_OutputsProfiles(t *testing.T) { if got[1].Name != "target" || got[1].Default { t.Fatalf("got[1] = %#v, want non-default target profile", got[1]) } + for i, item := range got { + if item.Active != item.Default { + t.Fatalf("got[%d]: active = %v, default = %v; the alias must mirror default", i, item.Active, item.Default) + } + } } func TestProfileListRun_NotConfiguredReturnsEmptyList(t *testing.T) {