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.
This commit is contained in:
luozhixiong
2026-07-15 11:16:26 +08:00
committed by review
parent cf2af70b98
commit 76ee4cd05e
4 changed files with 56 additions and 9 deletions

View File

@@ -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{

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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) {