From d2fd7e00b59e3dc734f5f402b9bf92f7aa4826bb Mon Sep 17 00:00:00 2001 From: "shangguanxuan.sgx" Date: Wed, 17 Jun 2026 18:47:51 +0800 Subject: [PATCH] fix: skip tui for recommend login --- docs/batch-auth-prd.json | 33 ++++++--- internal/app/auth_command.go | 55 +++++++++----- internal/app/auth_command_test.go | 118 +++++++++++++++++++++++------- 3 files changed, 151 insertions(+), 55 deletions(-) diff --git a/docs/batch-auth-prd.json b/docs/batch-auth-prd.json index 69fb87d1..c019714d 100644 --- a/docs/batch-auth-prd.json +++ b/docs/batch-auth-prd.json @@ -55,16 +55,26 @@ "steps": [ "User runs dws auth login --recommend.", "CLI completes OAuth loopback or device login, lets the user choose the DingTalk organization through the normal login flow, and saves the token.", - "After the login identity and organization are settled, interactive CLI shows the Feishu-like terminal authorization guide: 选择操作, then 授权范围选择.", - "CLI calls pat.batch_plan with recommend=true by default, or recommend=false when the user selected 全部授权, grantType=permanent, caller=auth_login_recommend, dryRun=true.", - "CLI then shows the product-domain selector with all product lines selected by default; user can toggle, filter, or confirm.", - "CLI re-plans with the selected productCodes before starting the grant flow.", + "CLI does not show the product-domain TUI for --recommend.", + "CLI calls pat.batch_plan with recommend=true, grantType=permanent, caller=auth_login_recommend, dryRun=true.", "PAT core loads the Diamond-driven recommended set, excludes non-recommended medium-risk scopes from the default plan, and returns selectedScopes plus warnings/skipped scopes.", "CLI calls pat.batch_grant with scopes=selectedScopes, startFlow=true, noWait=true, caller=auth_login_recommend.", "PAT core creates a single batch Device Code Flow and returns PAT_BATCH_AUTH_PENDING with flowId/userCode/uri.", "CLI opens uri, polls flow status, and retries the original grant after approval." ] }, + { + "name": "loginInteractiveTuiWithoutRecommend", + "steps": [ + "User runs dws auth login without --recommend in an interactive terminal.", + "CLI completes OAuth loopback or device login, lets the user choose the DingTalk organization through the normal login flow, and saves the token.", + "After the login identity and organization are settled, CLI shows the Feishu-like terminal authorization guide: 选择操作, then 授权范围选择.", + "CLI calls pat.batch_plan with recommend=true when the user selected 推荐授权, or recommend=false when the user selected 全部授权.", + "CLI shows the product-domain selector with all product lines selected by default; user can toggle, filter, or confirm.", + "CLI re-plans with the selected productCodes before starting the grant flow.", + "Without --yes, CLI calls pat.batch_grant with scopes=selectedScopes, startFlow=true, noWait=true, then opens/polls the returned authorization URL." + ] + }, { "name": "batchChmodWithoutYes", "steps": [ @@ -212,7 +222,7 @@ "branch": "codex/dws-auth-login-recommend", "commit": "2264743e78f71d3664b21d90e3a6a36f383beed7", "prepubPackage": "pushed to GitHub branch codex/dws-auth-login-recommend with CLI commit 2264743e78f71d3664b21d90e3a6a36f383beed7; rebuilt local default /usr/local/bin/dws -> /Users/xuan/bin/dws from current source; local go test ./internal/app ./internal/pat ./internal/errors ./internal/auth and go test ./test/unit passed. Still blocked_no_aone_app_for_current_github_repo_lippi_dws_release_uses_gitlab_release_repo for formal prepub package evidence.", - "prepubFunctionalVerification": "local_cli_tui_contract_ok: interactive default dws auth login --recommend now follows the terminal usage chain: 选择操作, 选择授权范围, then default-all-selected product domains before grant. 推荐授权 uses recommend=true; 全部授权 replans selected product domains with recommend=false. explicit --format json and any non-TTY stdin/stdout/stderr keep structured non-interactive mode. local_yes_contract_ok: dws auth login --recommend --yes skips the TUI/browser flow and calls pat.batch_grant without startFlow/noWait, preserving direct recommended direct grant behavior. current_head_e2e_pending_server_redeploy" + "prepubFunctionalVerification": "local_cli_tui_contract_ok: dws auth login --recommend now skips terminal TUI/product-domain selection and directly uses server recommend=true selectedScopes. Interactive default dws auth login without --recommend now follows the terminal usage chain after login/org selection: 选择操作, 选择授权范围, then default-all-selected product domains before grant. 推荐授权 uses recommend=true; 全部授权 replans selected product domains with recommend=false. explicit --format json and any non-TTY stdin/stdout/stderr keep structured non-interactive mode. local_yes_contract_ok: dws auth login --recommend --yes skips the TUI/browser flow and calls pat.batch_grant without startFlow/noWait, preserving direct recommended direct grant behavior. current_head_e2e_pending_server_redeploy" }, { "repo": "lippi-open-mcpserver-gw", @@ -354,6 +364,7 @@ "2026-06-17 CLI --yes follow-up: commit 3ec35e30e828b088a8e9415d76804f56feed6354 fixes dws auth login --recommend --yes to behave like batch auto-confirm: resolveAuthLoginConfig reads inherited --yes, recommend human TUI is skipped, and RunLoginRecommendAuthorizationWithOptions calls pat.batch_grant without startFlow/noWait so no browser authorization page is created. No --yes keeps the previous TUI plus batch authorization page flow. Verification passed: go test ./internal/pat -run TestLoginRecommendAuthorization; go test ./internal/app -run TestAuthLoginRecommendSelectorRespectsExplicitFormat|TestLoginRecommendProductLabelMatchesTUITarget|TestResolveAuthLoginConfigReadsInheritedYes; go test ./internal/app ./internal/pat ./internal/errors; go test ./test/unit; /usr/local/bin/dws -> /Users/xuan/bin/dws rebuilt from this commit.", "2026-06-17 CLI TUI chain correction: commit 2264743e78f71d3664b21d90e3a6a36f383beed7 changes TUI from a single product selector into the intended terminal usage chain from the MGDone screenshots. Interactive dws auth login --recommend now shows 选择操作 (直接使用CLI / 一键配置智能体应用 / 手动输入应用凭证), then 选择授权范围 (推荐授权 / 全部授权), then the default-all-selected 选择要授权的业务域 product-domain multiselect. 推荐授权 keeps recommend=true; 全部授权 replans with recommend=false for selected product domains. Complete runtime AppKey/AppSecret overrides now skip MCP clientId fetch so manual credentials are real rather than decorative. Verification passed: go test ./internal/pat -run TestLoginRecommendAuthorization; go test ./internal/app -run TestAuthLoginRecommendSelectorRespectsExplicitFormat|TestLoginRecommendProductLabelMatchesTUITarget|TestResolveAuthLoginConfigReadsInheritedYes; go test ./internal/auth -run TestDeviceFlow|TestOAuth|TestCredential|TestReset|TestAppConfig|TestClient; go test ./internal/app ./internal/pat ./internal/errors ./internal/auth; go test ./test/unit; /usr/local/bin/dws -> /Users/xuan/bin/dws rebuilt from this commit.", "2026-06-17 CLI TUI sequence correction: commit 537719c677a9c79d8c45115c7bbbb29fff8dfe1f moves the terminal authorization guide into runRecommend, so dws auth login --recommend first completes DingTalk login, organization selection, and token persistence, then starts 选择操作 -> 选择授权范围 -> default-all-selected 选择要授权的业务域. A new app-layer regression test asserts the token is already readable before any TUI selector runs. Verification passed: go test ./internal/app -run TestAuthLoginRecommendTUIRunsAfterLoginTokenSaved|TestAuthLoginRecommendSelectorRespectsExplicitFormat|TestLoginRecommendProductLabelMatchesTUITarget|TestResolveAuthLoginConfigReadsInheritedYes; go test ./internal/pat -run TestLoginRecommendAuthorization; go test ./internal/app ./internal/pat ./internal/errors ./internal/auth; go test ./test/unit; go build -o /Users/xuan/bin/dws ./cmd/main.go; /usr/local/bin/dws version --format json reports version dev, edition open.", + "2026-06-17 CLI TUI condition correction: dws auth login --recommend no longer shows 选择操作, 授权范围选择, or product-domain TUI. It directly calls pat.batch_plan recommend=true and grants the server-selected recommended scopes. The post-login TUI now belongs only to interactive dws auth login without --recommend, after login/org selection/token persistence. Verification target: TestAuthLoginRecommendSkipsPostLoginTUI and TestAuthLoginWithoutRecommendTUIRunsAfterLoginTokenSaved.", "2026-06-17 MGDone MCP receipt: Cursor mastergo-magic-mcp server was started from /Users/xuan/.cursor/mcp.json with token redacted. tools/list exposed mcp__getDesignSections/mcp__getDsl/mcp__getDesignSvgs/mcp__getDesignTexts. mcp__getDesignSections for fileId=187681481694834, layerId=74:36608 returned design-sections API not available; fallback mcp__getDsl with the same page_id returned an empty nodes/components DSL. Therefore current terminal implementation uses the visible MGDone screenshot target for TUI text/label/control behavior and records that a true layer_id/source_layer_id is still needed for pixel-level DSL extraction.", "2026-06-17 frontend account-row fix: commit f33e349b2c29c7ba139e48fdfbdd70759ce3a4c4 makes the authorization account row prefer userAvatar/userName before orgLogo/orgName to avoid mixed org icon + user name rendering. Verification: npm run build passed with existing bundle-size warnings only.", "Frontend O2 non-production task 149432767 succeeded for commit f33e349b2c29c7ba139e48fdfbdd70759ce3a4c4, flow 114592657, version 5.219.0, pub_env daily, build id 53073038, assetCount 203. CDN bundle contains userAvatar|| and no longer contains the old orgLogo|| account-row priority; old title 可授权的权限(已选 remains absent.", @@ -478,12 +489,12 @@ "/var/folders/hz/2sm0ql493cv6jngtjk7xsby00000gp/T/codex-clipboard-16c11a9e-7ae7-4703-aa66-4251f7b59f16.png", "/var/folders/hz/2sm0ql493cv6jngtjk7xsby00000gp/T/codex-clipboard-5aff04a6-9a45-4ba5-8741-b7abcd4f2303.png" ], - "usageGuide": "Interactive dws auth login --recommend starts the terminal authorization guide only after OAuth/device login, DingTalk organization selection, login summary, and token persistence are complete. The first guide screen is 使用引导 / 选择操作 with options 直接使用CLI, 一键配置智能体应用, 手动输入应用凭证. 直接使用CLI continues directly to authorization setup; manual credentials collect AppKey/AppSecret for subsequent runtime credential use and do not replace the already selected login organization.", + "usageGuide": "Interactive dws auth login without --recommend starts the terminal authorization guide only after OAuth/device login, DingTalk organization selection, login summary, and token persistence are complete. dws auth login --recommend skips this TUI entirely and uses the server recommended set. The first guide screen for the non-recommend TUI is 使用引导 / 选择操作 with options 直接使用CLI, 一键配置智能体应用, 手动输入应用凭证. 直接使用CLI continues directly to authorization setup; manual credentials collect AppKey/AppSecret for subsequent runtime credential use and do not replace the already selected login organization.", "authorizationScopeSelector": "After the usage guide, the TUI presents 授权范围选择 with 推荐授权 and 全部授权. 推荐授权 is the default safe path and calls pat.batch_plan with recommend=true. 全部授权 replans the selected product domains with recommend=false so PAT core/server risk handling decides pending/grant behavior.", - "loginRecommendSelector": "After the login is already complete and the usage/scope guides have run, interactive dws auth login --recommend presents 选择要授权的业务域 with all product domains selected by default.", + "loginRecommendSelector": "After the login is already complete and the usage/scope guides have run, interactive dws auth login without --recommend presents 选择要授权的业务域 with all product domains selected by default. The --recommend flag does not show this selector.", "rowFormat": "Product rows follow the MGDone terminal target: selected checkbox, left-aligned product code, Chinese product name, and short operation summary separated by a plain hyphen, such as approval 审批 - 审批实例,审批模板,审批任务管理.", "controls": "TUI must support space toggle, up/down navigation, slash filter, enter confirm, and ctrl+a select all through the underlying selector component. Description text is 空格选择 回车确认 while the component renders the detailed key hints.", - "nextStep": "Without --yes, after product confirmation the CLI prints/opens the complete encoded authorization URL returned by the server and waits for browser approval. With --yes, the CLI skips the product TUI and web authorization page and directly grants the service-selected safe scopes." + "nextStep": "Without --yes, --recommend directly prints/opens the complete encoded authorization URL returned by the server for the recommended scope set. In the non-recommend TUI path, the CLI prints/opens that URL after product confirmation. With --yes, the CLI skips TUI and web authorization page and directly grants the service-selected safe scopes when a direct-grant path is available." }, "securityRules": [ "Gateway derives orgId and uid from trusted token/header only.", @@ -497,8 +508,8 @@ "acceptanceCriteria": [ "Prepub package/build succeeds for every changed repo: dingtalk-workspace-cli, lippi-open-mcpserver-gw, lippi-pat-core-wukong-capability, lippi-open-app-dev, and lippi-open-dev-front.", "Prepub deployed versions include the current branch commits for CLI, gateway, PAT core, BFF, and frontend.", - "On prepub, dws auth login --recommend opens the batch authorization URL after successful login.", - "On prepub, interactive dws auth login --recommend first completes DingTalk login and organization selection, then displays the terminal authorization chain: 选择操作, 授权范围选择, and default-all-selected product-domain TUI before opening the authorization URL.", + "On prepub, dws auth login --recommend opens the batch authorization URL after successful login without showing terminal product-domain selection.", + "On prepub, interactive dws auth login without --recommend first completes DingTalk login and organization selection, then displays the terminal authorization chain: 选择操作, 授权范围选择, and default-all-selected product-domain TUI before opening the authorization URL.", "On prepub, dws auth login --recommend --yes does not open the authorization page and grants the service-selected recommended scopes directly.", "On prepub, dws pat chmod --recommend --product calendar without --yes opens the authorization URL.", "On prepub, dws pat chmod --recommend --product calendar --yes preserves the existing direct grant behavior.", @@ -515,5 +526,5 @@ "muskChiefOperator": "Review the product flow for speed, minimum surface area, and user clarity.", "computerUse": "Verify prepub/front-end page only after frontend repo and build credentials are available." }, - "updatedAt": "2026-06-17T17:52:22+08:00" + "updatedAt": "2026-06-17T18:37:31+08:00" } diff --git a/internal/app/auth_command.go b/internal/app/auth_command.go index eb6f09e1..e83a5ea8 100644 --- a/internal/app/auth_command.go +++ b/internal/app/auth_command.go @@ -114,8 +114,8 @@ func newAuthLoginCommand(patCaller edition.ToolCaller) *cobra.Command { configDir := defaultConfigDir() var tokenData *authpkg.TokenData format, _ := cmd.Root().PersistentFlags().GetString("format") - recommendHumanMode := !cfg.Yes && authLoginShouldUseRecommendHumanMode(cmd, format, cfg.Recommend) - recommendScopeMode := pat.LoginRecommendScopeRecommended + postLoginTUIMode := !cfg.Yes && authLoginShouldUsePostLoginTUIMode(cmd, format, cfg.Recommend) + humanAuthMode := !cfg.Yes && authLoginShouldUseHumanAuthorizationMode(cmd, format, cfg.Recommend || postLoginTUIMode) switch { case strings.TrimSpace(cfg.Token) != "": @@ -153,11 +153,12 @@ func newAuthLoginCommand(patCaller edition.ToolCaller) *cobra.Command { clearCompatCache() w := cmd.OutOrStdout() - runRecommend := func() error { - if !cfg.Recommend { + runPostLoginAuthorization := func() error { + if !cfg.Recommend && !postLoginTUIMode { return nil } - if recommendHumanMode { + recommendScopeMode := pat.LoginRecommendScopeRecommended + if postLoginTUIMode { action, err := authLoginGuideActionSelector() if err != nil { return err @@ -171,13 +172,13 @@ func newAuthLoginCommand(patCaller edition.ToolCaller) *cobra.Command { } } opts := pat.LoginRecommendOptions{Confirmed: cfg.Yes, ScopeMode: recommendScopeMode} - if recommendHumanMode { + if postLoginTUIMode { opts.ProductSelector = func(products []pat.LoginRecommendProduct) ([]string, error) { return loginRecommendProductSelector(products) } } retryFormat := format - if recommendHumanMode { + if humanAuthMode { retryFormat = "table" } run := func(ctx context.Context) error { @@ -197,8 +198,8 @@ func newAuthLoginCommand(patCaller edition.ToolCaller) *cobra.Command { } // Check if JSON output is requested - if strings.EqualFold(strings.TrimSpace(format), "json") && !recommendHumanMode { - if err := runRecommend(); err != nil { + if strings.EqualFold(strings.TrimSpace(format), "json") && !humanAuthMode { + if err := runPostLoginAuthorization(); err != nil { return err } return writeAuthLoginJSON(w, tokenData, cfg.Force) @@ -226,7 +227,7 @@ func newAuthLoginCommand(patCaller edition.ToolCaller) *cobra.Command { } } fmt.Fprintf(w, "Token 将自动刷新,无需重复登录\n") - return runRecommend() + return runPostLoginAuthorization() }, } cmd.Flags().String("token", "", "Access token") @@ -759,20 +760,38 @@ func selectLoginRecommendProducts(products []pat.LoginRecommendProduct) ([]strin return selected, nil } -func authLoginShouldShowRecommendSelector(cmd *cobra.Command, format string) bool { - return authLoginShouldUseRecommendHumanModeForTerminal(cmd, format, true, authLoginInteractiveTerminal()) +func authLoginShouldShowPostLoginTUI(cmd *cobra.Command, format string, recommend bool) bool { + return authLoginShouldUsePostLoginTUIModeForTerminal(cmd, format, recommend, authLoginInteractiveTerminal()) } -func authLoginShouldShowRecommendSelectorForTerminal(cmd *cobra.Command, format string, interactive bool) bool { - return authLoginShouldUseRecommendHumanModeForTerminal(cmd, format, true, interactive) +func authLoginShouldShowPostLoginTUIForTerminal(cmd *cobra.Command, format string, recommend bool, interactive bool) bool { + return authLoginShouldUsePostLoginTUIModeForTerminal(cmd, format, recommend, interactive) } -func authLoginShouldUseRecommendHumanMode(cmd *cobra.Command, format string, recommend bool) bool { - return authLoginShouldUseRecommendHumanModeForTerminal(cmd, format, recommend, authLoginInteractiveTerminal()) +func authLoginShouldUsePostLoginTUIMode(cmd *cobra.Command, format string, recommend bool) bool { + return authLoginShouldUsePostLoginTUIModeForTerminal(cmd, format, recommend, authLoginInteractiveTerminal()) } -func authLoginShouldUseRecommendHumanModeForTerminal(cmd *cobra.Command, format string, recommend bool, interactive bool) bool { - if !recommend || !interactive { +func authLoginShouldUsePostLoginTUIModeForTerminal(cmd *cobra.Command, format string, recommend bool, interactive bool) bool { + if recommend || !interactive { + return false + } + return authLoginAllowsInteractiveDefault(cmd, format) +} + +func authLoginShouldUseHumanAuthorizationMode(cmd *cobra.Command, format string, hasAuthorizationFlow bool) bool { + return authLoginShouldUseHumanAuthorizationModeForTerminal(cmd, format, hasAuthorizationFlow, authLoginInteractiveTerminal()) +} + +func authLoginShouldUseHumanAuthorizationModeForTerminal(cmd *cobra.Command, format string, hasAuthorizationFlow bool, interactive bool) bool { + if !hasAuthorizationFlow || !interactive { + return false + } + return authLoginAllowsInteractiveDefault(cmd, format) +} + +func authLoginAllowsInteractiveDefault(cmd *cobra.Command, format string) bool { + if cmd == nil || cmd.Root() == nil { return false } if !strings.EqualFold(strings.TrimSpace(format), "json") { diff --git a/internal/app/auth_command_test.go b/internal/app/auth_command_test.go index 4924ad45..3debf263 100644 --- a/internal/app/auth_command_test.go +++ b/internal/app/auth_command_test.go @@ -184,7 +184,7 @@ func TestAuthStatusRefreshFailureLeavesStoredTokenIntact(t *testing.T) { } } -func TestAuthLoginRecommendSelectorRespectsExplicitFormat(t *testing.T) { +func TestAuthLoginPostLoginTUIModeRespectsRecommendAndFormat(t *testing.T) { newRoot := func(t *testing.T) *cobra.Command { t.Helper() root := &cobra.Command{Use: "dws"} @@ -192,13 +192,23 @@ func TestAuthLoginRecommendSelectorRespectsExplicitFormat(t *testing.T) { return root } - t.Run("default json still shows selector for interactive login", func(t *testing.T) { + t.Run("recommend skips tui but keeps human auth for interactive login", func(t *testing.T) { root := newRoot(t) - if !authLoginShouldShowRecommendSelectorForTerminal(root, "json", true) { - t.Fatal("default json format should still show recommend selector in an interactive terminal") + if authLoginShouldShowPostLoginTUIForTerminal(root, "json", true, true) { + t.Fatal("--recommend must not show the post-login product TUI") } - if !authLoginShouldUseRecommendHumanModeForTerminal(root, "json", true, true) { - t.Fatal("default json format should use human recommend flow in an interactive terminal") + if !authLoginShouldUseHumanAuthorizationModeForTerminal(root, "json", true, true) { + t.Fatal("default interactive --recommend should still use human authorization flow") + } + }) + + t.Run("without recommend shows tui for interactive default login", func(t *testing.T) { + root := newRoot(t) + if !authLoginShouldShowPostLoginTUIForTerminal(root, "json", false, true) { + t.Fatal("default interactive login without --recommend should show post-login TUI") + } + if !authLoginShouldUseHumanAuthorizationModeForTerminal(root, "json", true, true) { + t.Fatal("default interactive post-login TUI flow should use human authorization flow") } }) @@ -207,41 +217,41 @@ func TestAuthLoginRecommendSelectorRespectsExplicitFormat(t *testing.T) { if err := root.PersistentFlags().Set("format", "json"); err != nil { t.Fatalf("set format: %v", err) } - if authLoginShouldShowRecommendSelectorForTerminal(root, "json", true) { - t.Fatal("explicit --format json must not show recommend selector") + if authLoginShouldShowPostLoginTUIForTerminal(root, "json", false, true) { + t.Fatal("explicit --format json must not show post-login TUI") } - if authLoginShouldUseRecommendHumanModeForTerminal(root, "json", true, true) { - t.Fatal("explicit --format json must keep machine-readable recommend flow") + if authLoginShouldUseHumanAuthorizationModeForTerminal(root, "json", true, true) { + t.Fatal("explicit --format json must keep machine-readable authorization flow") } }) - t.Run("table shows selector", func(t *testing.T) { + t.Run("table without recommend shows tui", func(t *testing.T) { root := newRoot(t) if err := root.PersistentFlags().Set("format", "table"); err != nil { t.Fatalf("set format: %v", err) } - if !authLoginShouldShowRecommendSelectorForTerminal(root, "table", true) { - t.Fatal("table format should show recommend selector in an interactive terminal") + if !authLoginShouldShowPostLoginTUIForTerminal(root, "table", false, true) { + t.Fatal("table format should show post-login TUI without --recommend") } - if !authLoginShouldUseRecommendHumanModeForTerminal(root, "table", true, true) { - t.Fatal("table format should use human recommend flow in an interactive terminal") + if !authLoginShouldUseHumanAuthorizationModeForTerminal(root, "table", true, true) { + t.Fatal("table format should use human authorization flow in an interactive terminal") } }) t.Run("non interactive skips selector", func(t *testing.T) { root := newRoot(t) - if authLoginShouldShowRecommendSelectorForTerminal(root, "json", false) { - t.Fatal("non-interactive login should skip recommend selector") + if authLoginShouldShowPostLoginTUIForTerminal(root, "json", false, false) { + t.Fatal("non-interactive login should skip post-login TUI") } - if authLoginShouldUseRecommendHumanModeForTerminal(root, "json", true, false) { - t.Fatal("non-interactive login should keep machine-readable recommend flow") + if authLoginShouldUseHumanAuthorizationModeForTerminal(root, "json", true, false) { + t.Fatal("non-interactive login should keep machine-readable authorization flow") } }) - t.Run("without recommend keeps normal login output contract", func(t *testing.T) { + t.Run("without authorization flow keeps normal login output contract", func(t *testing.T) { root := newRoot(t) - if authLoginShouldUseRecommendHumanModeForTerminal(root, "json", false, true) { - t.Fatal("login without --recommend should not switch default json to human mode") + if authLoginShouldUseHumanAuthorizationModeForTerminal(root, "json", false, true) { + t.Fatal("login without a post-login authorization flow should not switch default json to human mode") } }) } @@ -287,7 +297,63 @@ func TestResolveAuthLoginConfigReadsInheritedYes(t *testing.T) { } } -func TestAuthLoginRecommendTUIRunsAfterLoginTokenSaved(t *testing.T) { +func TestAuthLoginRecommendSkipsPostLoginTUI(t *testing.T) { + t.Setenv(keychain.DisableKeychainEnv, "1") + t.Setenv(keychain.StorageDirEnv, t.TempDir()) + t.Setenv("DWS_CONFIG_DIR", t.TempDir()) + + oldGuideSelector := authLoginGuideActionSelector + oldGuideApplier := authLoginGuideActionApplier + oldScopeSelector := loginRecommendScopeModeSelector + oldProductSelector := loginRecommendProductSelector + oldInteractiveTerminal := authLoginInteractiveTerminal + t.Cleanup(func() { + authLoginGuideActionSelector = oldGuideSelector + authLoginGuideActionApplier = oldGuideApplier + loginRecommendScopeModeSelector = oldScopeSelector + loginRecommendProductSelector = oldProductSelector + authLoginInteractiveTerminal = oldInteractiveTerminal + }) + authLoginInteractiveTerminal = func() bool { return true } + authLoginGuideActionSelector = func() (authLoginGuideAction, error) { + t.Fatal("--recommend must not call the post-login guide selector") + return "", nil + } + authLoginGuideActionApplier = func(*cobra.Command, string, authLoginGuideAction) error { + t.Fatal("--recommend must not apply a post-login guide action") + return nil + } + loginRecommendScopeModeSelector = func() (pat.LoginRecommendScopeMode, error) { + t.Fatal("--recommend must not call the scope-mode TUI") + return "", nil + } + loginRecommendProductSelector = func([]pat.LoginRecommendProduct) ([]string, error) { + t.Fatal("--recommend must not call the product-domain TUI") + return nil, nil + } + + fake := &authLoginRecommendSequenceCaller{responses: []string{ + `{"success":true,"data":{"items":[{"scope":"calendar.event:read","productCode":"calendar","productName":"日历"}],"selectedScopes":["calendar.event:read"]}}`, + `{"success":true,"data":{"grantedScopes":["calendar.event:read"]}}`, + }} + cmd := newAuthLoginCommand(fake) + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"--token", "login-token", "--recommend"}) + + if err := cmd.Execute(); err != nil { + t.Fatalf("auth login --recommend error = %v\noutput:\n%s", err, out.String()) + } + if len(fake.tools) != 2 { + t.Fatalf("CallTool count = %d, want plan + grant", len(fake.tools)) + } + if fake.tools[0] != "pat.batch_plan" || fake.tools[1] != "pat.batch_grant" { + t.Fatalf("tool sequence = %v, want plan, grant", fake.tools) + } +} + +func TestAuthLoginWithoutRecommendTUIRunsAfterLoginTokenSaved(t *testing.T) { t.Setenv(keychain.DisableKeychainEnv, "1") t.Setenv(keychain.StorageDirEnv, t.TempDir()) configDir := t.TempDir() @@ -347,13 +413,13 @@ func TestAuthLoginRecommendTUIRunsAfterLoginTokenSaved(t *testing.T) { var out bytes.Buffer cmd.SetOut(&out) cmd.SetErr(&out) - cmd.SetArgs([]string{"--token", "login-token", "--recommend"}) + cmd.SetArgs([]string{"--token", "login-token"}) if err := cmd.Execute(); err != nil { - t.Fatalf("auth login --recommend error = %v\noutput:\n%s", err, out.String()) + t.Fatalf("auth login error = %v\noutput:\n%s", err, out.String()) } if !sawTokenBeforeTUI { - t.Fatal("recommend TUI selector was not called") + t.Fatal("post-login TUI selector was not called") } if len(fake.tools) != 3 { t.Fatalf("CallTool count = %d, want initial plan + selected plan + grant", len(fake.tools))