@pippit-dev/cli 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +23 -2
  2. package/checksums.txt +6 -6
  3. package/cmd/auth/auth.go +136 -142
  4. package/cmd/auth/auth_test.go +134 -0
  5. package/cmd/canvas/canvas.go +235 -0
  6. package/cmd/canvas/canvas_test.go +126 -0
  7. package/cmd/root.go +29 -4
  8. package/cmd/root_test.go +60 -0
  9. package/cmd/short_drama_test.go +4 -7
  10. package/cmd/update/update.go +34 -5
  11. package/cmd/update/update_test.go +84 -0
  12. package/internal/auth/auth_test.go +813 -0
  13. package/internal/auth/browser_darwin.go +14 -0
  14. package/internal/auth/browser_env.go +35 -0
  15. package/internal/auth/browser_linux.go +14 -0
  16. package/internal/auth/browser_windows.go +14 -0
  17. package/internal/auth/identity.go +89 -0
  18. package/internal/auth/loopback.go +324 -0
  19. package/internal/auth/manager.go +338 -144
  20. package/internal/auth/store.go +303 -0
  21. package/internal/auth/store_file_unix.go +176 -0
  22. package/internal/auth/store_file_windows.go +11 -0
  23. package/internal/auth/types.go +72 -0
  24. package/internal/canvas/allocate.go +70 -0
  25. package/internal/canvas/apply.go +203 -0
  26. package/internal/canvas/canvas_test.go +446 -0
  27. package/internal/canvas/create.go +380 -0
  28. package/internal/canvas/get.go +156 -0
  29. package/internal/canvas/types.go +68 -0
  30. package/internal/canvas/upload.go +250 -0
  31. package/internal/common/access_key.go +42 -6
  32. package/internal/common/access_key_test.go +113 -0
  33. package/internal/common/client.go +119 -21
  34. package/internal/common/client_test.go +213 -0
  35. package/internal/common/runner.go +15 -0
  36. package/internal/config/config.go +0 -20
  37. package/internal/config/config_test.go +0 -18
  38. package/package.json +1 -1
  39. package/skills/short-drama/SKILL.md +4 -4
  40. package/skills/xyq-nest-skill/SKILL.md +11 -1
package/README.md CHANGED
@@ -205,7 +205,7 @@ python3 skills/xyq-nest-skill/scripts/download_results.py \
205
205
 
206
206
  ```bash
207
207
  npx @pippit-dev/cli@latest install
208
- export XYQ_ACCESS_KEY="<access-key>"
208
+ pippit-tool-cli login
209
209
  pippit-tool-cli --version
210
210
  pippit-tool-cli short-drama +submit-run --message "写一个赛博朋克短剧开头"
211
211
  pippit-tool-cli short-drama +upload-file --path ./reference.doc
@@ -222,6 +222,25 @@ pippit-tool-cli download-result --output-path ./thread_123/results/result.mp4 --
222
222
 
223
223
  短剧命令的错误日志会追加写入本地每日日志文件:`~/.pippit_tool_cli/logs/yyyy-mm-dd.log`。日志路径会基于当前用户主目录和系统路径分隔符生成,因此可在 macOS、Linux 和 Windows 上使用。
224
224
 
225
+ ## Canvas 原子命令
226
+
227
+ CLI 提供个人漫剧画布的通用原子命令,不包含特定来源的导入或转换逻辑:
228
+
229
+ ```bash
230
+ # 首次使用时打开小云雀网页授权
231
+ pippit-tool-cli login
232
+ pippit-tool-cli status
233
+
234
+ # 创建、分配资产 ID、查询、上传与提交单个画布 transaction
235
+ pippit-tool-cli canvas create --title "CLI Canvas" --wait
236
+ pippit-tool-cli canvas allocate --count 3
237
+ pippit-tool-cli canvas get --asset-id PIPPIT_ASSET_ID
238
+ pippit-tool-cli canvas upload --path ./reference.png
239
+ pippit-tool-cli canvas apply --project-id PROJECT_ID --file ./patch.json
240
+ ```
241
+
242
+ 五个命令均输出单行 JSON,资源 ID 保持字符串。`allocate` 只预留 ID,实际资产仍由后续 `apply` transaction 创建。`create` 的 `request_id` 用于追踪,不是跨服务崩溃窗口的严格幂等键;写请求结果不明确时不要盲目重放,应先使用 `canvas get` 回读确认。`apply` 当前只接受一个 transaction,但该 transaction 可以包含多个 patches;CLI 会严格检查 transaction ACK 和每个目标资产的新版本。
243
+
225
244
  ## 生图 CLI
226
245
 
227
246
  `generate-image` 会上传本地参考图片,然后向综合 Nest Agent 提交生图请求:
@@ -333,4 +352,6 @@ pippit-tool-cli query-result \
333
352
 
334
353
  ## 鉴权
335
354
 
336
- `short-drama +submit-run`、`get-thread`、`list-thread-file`、`short-drama +upload-file` 以及 `xyq-skill` Python 脚本都使用 `Authorization: Bearer <XYQ_ACCESS_KEY>` 鉴权。OAuth 命令代码仍保留在仓库中,但短剧运行时请求不使用 OAuth。
355
+ 原生 CLI 命令通过 `pippit-tool-cli login` 打开小云雀网页授权,并把本机设备专属凭证保存到系统安全凭证库;Access Key 不会显示在终端。可用 `pippit-tool-cli status` 查看状态、`pippit-tool-cli logout` 清除本机登录。
356
+
357
+ CI 或 Agent 可继续显式设置 `XYQ_ACCESS_KEY`,它会覆盖本机网页登录凭证;配置错误时不会静默回退到个人登录。`skills/xyq-nest-skill/scripts` 下的独立 Python 脚本尚未接入原生 CLI 凭证库,当前仍需要该环境变量。
package/checksums.txt CHANGED
@@ -1,6 +1,6 @@
1
- d6750507d8f6aa3c5ca7eccc020eda460d8d6d928de927da9a977696e36dd41d pippit-tool-cli-1.0.17-darwin-amd64.tar.gz
2
- 1583340a524e123f9458044468c1a2950d36b0eb79f85c19f4801811565caf87 pippit-tool-cli-1.0.17-darwin-arm64.tar.gz
3
- 8a6bc206694a141a860971964b4c7493bfec21539c85b406092be6aae7712269 pippit-tool-cli-1.0.17-linux-amd64.tar.gz
4
- 5e25609c585e4f6e02ae7adc2eb0e4be7d14c35152f23d6113070b3e679617ab pippit-tool-cli-1.0.17-linux-arm64.tar.gz
5
- ef883ce5b445bd41774303729fefda216cce17610131f9a34b5f441ad6607cfd pippit-tool-cli-1.0.17-windows-amd64.zip
6
- ff47a915c07718b40e6342e35b21091f8db84ea7efd1adc351da274f5a2c6e62 pippit-tool-cli-1.0.17-windows-arm64.zip
1
+ 34184e1dab803c7c25acb2ef2b95c0358afe39aaf5423140342b5c12e1e80bb3 pippit-tool-cli-1.0.18-darwin-amd64.tar.gz
2
+ 0e66993f2b09950e206ee9e9f36c8a8994f6413fffc6a38ef4808457d66eb218 pippit-tool-cli-1.0.18-darwin-arm64.tar.gz
3
+ 24dbe1e620a760d7b43636f881e7716e0beefa32f81e45311983fc6a6fc94acf pippit-tool-cli-1.0.18-linux-amd64.tar.gz
4
+ 1cdb7cbf054ef1203579cd1c8e71035ce0c434cbfaed7dd3f1bc35605bfedd72 pippit-tool-cli-1.0.18-linux-arm64.tar.gz
5
+ 9028a53a9ac03967e245f4e1348bdea82f03dcd30dc85a72c30bcf744ff1bd56 pippit-tool-cli-1.0.18-windows-amd64.zip
6
+ 6fe7de9b87de3bdd41f7eefb6514e9c48acd079fdc4ff2c5e917b03d01a8ecc0 pippit-tool-cli-1.0.18-windows-arm64.zip
package/cmd/auth/auth.go CHANGED
@@ -1,144 +1,138 @@
1
1
  package authcmd
2
2
 
3
- //import (
4
- // "fmt"
5
- // "io"
6
- // "strings"
7
- // "time"
8
- //
9
- // "github.com/Pippit-dev/pippit-cli/internal/auth"
10
- // "github.com/Pippit-dev/pippit-cli/internal/common"
11
- // "github.com/bytedance/sonic"
12
- // "github.com/spf13/cobra"
13
- //)
14
- //
15
- //type checkResult struct {
16
- // Pending bool `json:"pending"`
17
- // State any `json:"state,omitempty"`
18
- //}
19
- //
20
- //func NewCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
21
- // cmd := &cobra.Command{
22
- // Use: "auth",
23
- // Short: "Manage Pippit OAuth login state",
24
- // }
25
- // cmd.SetOut(stdout)
26
- // cmd.SetErr(stderr)
27
- // cmd.AddCommand(newLoginCommand(stdout, stderr, runner))
28
- // cmd.AddCommand(newCheckCommand(stdout, stderr, runner))
29
- // cmd.AddCommand(newStatusCommand(stdout, stderr, runner))
30
- // cmd.AddCommand(newLogoutCommand(stdout, stderr, runner))
31
- // return cmd
32
- //}
33
- //
34
- //func newLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
35
- // cmd := &cobra.Command{
36
- // Use: "login",
37
- // Short: "Start an OAuth device login flow",
38
- // Args: cobra.NoArgs,
39
- // RunE: func(cmd *cobra.Command, _ []string) error {
40
- // if runner == nil || runner.AuthAuthorizer == nil {
41
- // return fmt.Errorf("auth manager is required")
42
- // }
43
- // flow, err := runner.AuthAuthorizer.NewLoginFlow(cmd.Context())
44
- // if err != nil {
45
- // return err
46
- // }
47
- // return writeJSON(stdout, flow)
48
- // },
49
- // }
50
- // cmd.SetOut(stdout)
51
- // cmd.SetErr(stderr)
52
- // return cmd
53
- //}
54
- //
55
- //func newCheckCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
56
- // var deviceCode string
57
- // cmd := &cobra.Command{
58
- // Use: "check",
59
- // Short: "Check whether an OAuth device login has completed",
60
- // Args: cobra.NoArgs,
61
- // RunE: func(cmd *cobra.Command, _ []string) error {
62
- // deviceCode = strings.TrimSpace(deviceCode)
63
- // if deviceCode == "" {
64
- // return fmt.Errorf("--device-code is required")
65
- // }
66
- // if runner == nil || runner.AuthAuthorizer == nil {
67
- // return fmt.Errorf("auth manager is required")
68
- // }
69
- // state, err := runner.AuthAuthorizer.CheckLogin(cmd.Context(), deviceCode)
70
- // if auth.IsLoginPending(err) {
71
- // return writeJSON(stdout, checkResult{Pending: true})
72
- // }
73
- // if err != nil {
74
- // return err
75
- // }
76
- // v := map[string]any{
77
- // "logged_in": state.LoggedIn,
78
- // "expires_at": state.ExpiresAt.Format(time.RFC3339),
79
- // }
80
- // return writeJSON(stdout, checkResult{State: v})
81
- // },
82
- // }
83
- // cmd.SetOut(stdout)
84
- // cmd.SetErr(stderr)
85
- // cmd.Flags().StringVar(&deviceCode, "device-code", "", "device code returned by auth login")
86
- // return cmd
87
- //}
88
- //
89
- //func newStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
90
- // cmd := &cobra.Command{
91
- // Use: "status",
92
- // Short: "Show current OAuth login state",
93
- // Args: cobra.NoArgs,
94
- // RunE: func(cmd *cobra.Command, _ []string) error {
95
- // if runner == nil || runner.AuthAuthorizer == nil {
96
- // return fmt.Errorf("auth manager is required")
97
- // }
98
- // state, err := runner.AuthAuthorizer.State(cmd.Context())
99
- // if err != nil {
100
- // return err
101
- // }
102
- // if !state.LoggedIn {
103
- // return fmt.Errorf("not logged in")
104
- // }
105
- // v := map[string]any{
106
- // "logged_in": state.LoggedIn,
107
- // "expires_at": state.ExpiresAt.Format(time.RFC3339),
108
- // }
109
- // return writeJSON(stdout, v)
110
- // },
111
- // }
112
- // cmd.SetOut(stdout)
113
- // cmd.SetErr(stderr)
114
- // return cmd
115
- //}
116
- //
117
- //func newLogoutCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
118
- // cmd := &cobra.Command{
119
- // Use: "logout",
120
- // Short: "Clear local OAuth login state",
121
- // Args: cobra.NoArgs,
122
- // RunE: func(cmd *cobra.Command, _ []string) error {
123
- // if runner == nil || runner.AuthAuthorizer == nil {
124
- // return fmt.Errorf("auth manager is required")
125
- // }
126
- // if err := runner.AuthAuthorizer.Logout(cmd.Context()); err != nil {
127
- // return err
128
- // }
129
- // return writeJSON(stdout, map[string]bool{"logged_out": true})
130
- // },
131
- // }
132
- // cmd.SetOut(stdout)
133
- // cmd.SetErr(stderr)
134
- // return cmd
135
- //}
136
- //
137
- //func writeJSON(w io.Writer, v any) error {
138
- // data, err := sonic.Marshal(v)
139
- // if err != nil {
140
- // return err
141
- // }
142
- // _, err = fmt.Fprintln(w, string(data))
143
- // return err
144
- //}
3
+ import (
4
+ "encoding/json"
5
+ "errors"
6
+ "fmt"
7
+ "io"
8
+ "strings"
9
+ "time"
10
+
11
+ internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
12
+ "github.com/Pippit-dev/pippit-cli/internal/common"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ type loginResult struct {
17
+ LoggedIn bool `json:"logged_in"`
18
+ Source string `json:"source"`
19
+ UID string `json:"uid,omitempty"`
20
+ CredentialScope string `json:"credential_scope,omitempty"`
21
+ ExpiresAt string `json:"expires_at,omitempty"`
22
+ }
23
+
24
+ type logoutResult struct {
25
+ LoggedOut bool `json:"logged_out"`
26
+ EnvironmentStillActive bool `json:"environment_still_active,omitempty"`
27
+ RemoteCredentialPreserved bool `json:"remote_credential_preserved"`
28
+ }
29
+
30
+ // NewLoginCommand creates the top-level `pippit-tool-cli login` command.
31
+ func NewLoginCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
32
+ var forceRefresh bool
33
+ command := &cobra.Command{
34
+ Use: "login",
35
+ Short: "通过浏览器登录小云雀 CLI",
36
+ Args: cobra.NoArgs,
37
+ RunE: func(command *cobra.Command, _ []string) error {
38
+ manager, err := requireAuthManager(runner)
39
+ if err != nil {
40
+ return err
41
+ }
42
+ credential, err := manager.Login(command.Context(), internal_auth.LoginOptions{
43
+ Progress: stderr, ForceRefresh: forceRefresh,
44
+ })
45
+ if err != nil {
46
+ return err
47
+ }
48
+ if runner.Config != nil && strings.TrimSpace(runner.Config.AccessKey) != "" {
49
+ _, _ = fmt.Fprintln(stderr, "提示:当前进程设置了 XYQ_ACCESS_KEY,它会继续优先于刚保存的浏览器登录凭证。")
50
+ }
51
+ return writeJSON(stdout, loginResult{
52
+ LoggedIn: true,
53
+ Source: "browser",
54
+ UID: credential.UID,
55
+ CredentialScope: credential.CredentialScope,
56
+ ExpiresAt: time.Unix(credential.ExpiredAt, 0).Format(time.RFC3339),
57
+ })
58
+ },
59
+ }
60
+ command.SetOut(stdout)
61
+ command.SetErr(stderr)
62
+ command.Flags().BoolVar(&forceRefresh, "force", false, "强制轮换当前设备的 CLI Access Key(仅在旧密钥被拒绝时使用)")
63
+ return command
64
+ }
65
+
66
+ // NewStatusCommand creates the top-level `pippit-tool-cli status` command.
67
+ func NewStatusCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
68
+ command := &cobra.Command{
69
+ Use: "status",
70
+ Short: "查看小云雀 CLI 登录状态",
71
+ Args: cobra.NoArgs,
72
+ RunE: func(command *cobra.Command, _ []string) error {
73
+ manager, err := requireAuthManager(runner)
74
+ if err != nil {
75
+ return err
76
+ }
77
+ status, err := manager.Status(command.Context())
78
+ if err != nil {
79
+ return err
80
+ }
81
+ result := loginResult{LoggedIn: status.LoggedIn, Source: status.Source, UID: status.UID, CredentialScope: status.CredentialScope}
82
+ if !status.ExpiresAt.IsZero() {
83
+ result.ExpiresAt = status.ExpiresAt.Format(time.RFC3339)
84
+ }
85
+ return writeJSON(stdout, result)
86
+ },
87
+ }
88
+ command.SetOut(stdout)
89
+ command.SetErr(stderr)
90
+ return command
91
+ }
92
+
93
+ // NewLogoutCommand clears only the browser-managed local credential. An
94
+ // explicit XYQ_ACCESS_KEY belongs to the caller's environment and is never
95
+ // modified or revoked by this command.
96
+ func NewLogoutCommand(stdout, stderr io.Writer, runner *common.Runner) *cobra.Command {
97
+ command := &cobra.Command{
98
+ Use: "logout",
99
+ Short: "清除本机小云雀 CLI 登录(不撤销远程 Access Key)",
100
+ Args: cobra.NoArgs,
101
+ RunE: func(command *cobra.Command, _ []string) error {
102
+ manager, err := requireAuthManager(runner)
103
+ if err != nil {
104
+ return err
105
+ }
106
+ if err := manager.Logout(command.Context(), false); err != nil && !errors.Is(err, internal_auth.ErrCredentialNotFound) {
107
+ return err
108
+ }
109
+ environmentActive := runner.Config != nil && strings.TrimSpace(runner.Config.AccessKey) != ""
110
+ if environmentActive {
111
+ _, _ = fmt.Fprintln(stderr, "提示:XYQ_ACCESS_KEY 仍由当前 shell 提供,CLI 无法替你清除该环境变量。")
112
+ }
113
+ _, _ = fmt.Fprintln(stderr, "已清除本机登录密钥;远程 Access Key 未撤销,并保留非秘密设备标识供下次登录安全复用。")
114
+ return writeJSON(stdout, logoutResult{
115
+ LoggedOut: true, EnvironmentStillActive: environmentActive, RemoteCredentialPreserved: true,
116
+ })
117
+ },
118
+ }
119
+ command.SetOut(stdout)
120
+ command.SetErr(stderr)
121
+ return command
122
+ }
123
+
124
+ func requireAuthManager(runner *common.Runner) (common.AuthManager, error) {
125
+ if runner == nil || runner.Auth == nil {
126
+ return nil, fmt.Errorf("小云雀 CLI 浏览器授权尚未配置")
127
+ }
128
+ return runner.Auth, nil
129
+ }
130
+
131
+ func writeJSON(writer io.Writer, value any) error {
132
+ payload, err := json.Marshal(value)
133
+ if err != nil {
134
+ return fmt.Errorf("编码登录结果失败: %w", err)
135
+ }
136
+ _, err = fmt.Fprintln(writer, string(payload))
137
+ return err
138
+ }
@@ -0,0 +1,134 @@
1
+ package authcmd
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "io"
8
+ "strings"
9
+ "testing"
10
+ "time"
11
+
12
+ internal_auth "github.com/Pippit-dev/pippit-cli/internal/auth"
13
+ "github.com/Pippit-dev/pippit-cli/internal/common"
14
+ "github.com/Pippit-dev/pippit-cli/internal/config"
15
+ )
16
+
17
+ type fakeAuthManager struct {
18
+ credential *internal_auth.Credential
19
+ status *internal_auth.Status
20
+ loginProgress string
21
+ loginCalls int
22
+ loginOptions []internal_auth.LoginOptions
23
+ logoutCalls int
24
+ }
25
+
26
+ func (manager *fakeAuthManager) ResolveAccessKey(context.Context) (string, error) {
27
+ if manager.credential == nil {
28
+ return "", internal_auth.ErrCredentialNotFound
29
+ }
30
+ return manager.credential.AccessKey, nil
31
+ }
32
+
33
+ func (manager *fakeAuthManager) Login(_ context.Context, options internal_auth.LoginOptions) (*internal_auth.Credential, error) {
34
+ manager.loginCalls++
35
+ manager.loginOptions = append(manager.loginOptions, options)
36
+ if options.Progress != nil {
37
+ message := manager.loginProgress
38
+ if message == "" {
39
+ message = "正在完成浏览器授权…\n"
40
+ }
41
+ _, _ = options.Progress.Write([]byte(message))
42
+ }
43
+ return manager.credential, nil
44
+ }
45
+
46
+ func TestLoginCommandCanRequestSafeCredentialRotation(t *testing.T) {
47
+ manager := &fakeAuthManager{credential: &internal_auth.Credential{
48
+ UID: "123", CredentialScope: "account-device-scope", ExpiredAt: time.Now().Add(time.Hour).Unix(),
49
+ }}
50
+ command := NewLoginCommand(io.Discard, io.Discard, &common.Runner{Config: &config.Config{}, Auth: manager})
51
+ command.SetArgs([]string{"--force"})
52
+ if err := command.Execute(); err != nil {
53
+ t.Fatal(err)
54
+ }
55
+ if len(manager.loginOptions) != 1 || !manager.loginOptions[0].ForceRefresh {
56
+ t.Fatalf("login options = %#v, want forced refresh", manager.loginOptions)
57
+ }
58
+ }
59
+
60
+ func (manager *fakeAuthManager) Status(context.Context) (*internal_auth.Status, error) {
61
+ return manager.status, nil
62
+ }
63
+
64
+ func (manager *fakeAuthManager) Logout(context.Context, bool) error {
65
+ manager.logoutCalls++
66
+ return nil
67
+ }
68
+
69
+ func (manager *fakeAuthManager) CredentialScope(context.Context) (string, error) {
70
+ return "device-scope", nil
71
+ }
72
+
73
+ func TestLoginCommandPrintsMetadataWithoutAccessKey(t *testing.T) {
74
+ const accessKey = "must-never-be-printed"
75
+ const loginURL = "https://xyq.jianying.com/cli/pippit-tool-login?callback=http%3A%2F%2F127.0.0.1%3A1234&random_secret_key=one-time-secret"
76
+ expiresAt := time.Now().Add(time.Hour).Unix()
77
+ manager := &fakeAuthManager{credential: &internal_auth.Credential{
78
+ AccessKey: accessKey, UID: "123", CredentialScope: "device-scope", ExpiredAt: expiresAt,
79
+ }, loginProgress: "正在完成浏览器授权…\n授权地址:\n" + loginURL + "\n"}
80
+ var stdout, stderr bytes.Buffer
81
+ command := NewLoginCommand(&stdout, &stderr, &common.Runner{Config: &config.Config{}, Auth: manager})
82
+ command.SetArgs(nil)
83
+ if err := command.Execute(); err != nil {
84
+ t.Fatalf("Execute() error = %v", err)
85
+ }
86
+ if manager.loginCalls != 1 || !strings.Contains(stderr.String(), "浏览器授权") {
87
+ t.Fatalf("login calls/stderr = %d/%q", manager.loginCalls, stderr.String())
88
+ }
89
+ if strings.Contains(stdout.String(), accessKey) || strings.Contains(stderr.String(), accessKey) {
90
+ t.Fatalf("command output leaked Access Key: stdout=%q stderr=%q", stdout.String(), stderr.String())
91
+ }
92
+ if !strings.Contains(stderr.String(), loginURL) || strings.Contains(stdout.String(), loginURL) {
93
+ t.Fatalf("login URL stream contract violated: stdout=%q stderr=%q", stdout.String(), stderr.String())
94
+ }
95
+ var result loginResult
96
+ if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
97
+ t.Fatal(err)
98
+ }
99
+ if !result.LoggedIn || result.Source != "browser" || result.UID != "123" || result.CredentialScope != "device-scope" {
100
+ t.Fatalf("login result = %#v", result)
101
+ }
102
+ }
103
+
104
+ func TestStatusAndLogoutDoNotModifyExplicitEnvironmentCredential(t *testing.T) {
105
+ manager := &fakeAuthManager{status: &internal_auth.Status{LoggedIn: true, Source: "environment"}}
106
+ runner := &common.Runner{Config: &config.Config{AccessKey: "explicit-ci-key"}, Auth: manager}
107
+ var statusOut, statusErr bytes.Buffer
108
+ status := NewStatusCommand(&statusOut, &statusErr, runner)
109
+ if err := status.Execute(); err != nil {
110
+ t.Fatal(err)
111
+ }
112
+ if strings.Contains(statusOut.String(), runner.Config.AccessKey) {
113
+ t.Fatal("status output leaked explicit Access Key")
114
+ }
115
+
116
+ var logoutOut, logoutErr bytes.Buffer
117
+ logout := NewLogoutCommand(&logoutOut, &logoutErr, runner)
118
+ if err := logout.Execute(); err != nil {
119
+ t.Fatal(err)
120
+ }
121
+ if manager.logoutCalls != 1 || runner.Config.AccessKey != "explicit-ci-key" {
122
+ t.Fatalf("logout calls/key = %d/%q", manager.logoutCalls, runner.Config.AccessKey)
123
+ }
124
+ if !strings.Contains(logoutErr.String(), "无法替你清除") || strings.Contains(logoutErr.String(), runner.Config.AccessKey) {
125
+ t.Fatalf("logout stderr = %q", logoutErr.String())
126
+ }
127
+ var result logoutResult
128
+ if err := json.Unmarshal(logoutOut.Bytes(), &result); err != nil {
129
+ t.Fatal(err)
130
+ }
131
+ if !result.LoggedOut || !result.RemoteCredentialPreserved || !strings.Contains(logoutErr.String(), "远程 Access Key 未撤销") {
132
+ t.Fatalf("logout result/stderr = %#v/%q", result, logoutErr.String())
133
+ }
134
+ }