174 lines
6.4 KiB
Go
174 lines
6.4 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestDevAppRobotConnectRegistered confirms `dev connect` is wired into
|
|
// the robot subtree alongside the (reused) provisioning commands.
|
|
func TestDevAppRobotConnectRegistered(t *testing.T) {
|
|
root := newDevAppCommand(&captureRunner{})
|
|
if _, _, err := root.Find([]string{"robot", "connect"}); err != nil {
|
|
t.Fatalf("dev connect not registered: %v", err)
|
|
}
|
|
// Provisioning stays under robot (reused, not duplicated by connect).
|
|
for _, name := range []string{"create", "submit", "result"} {
|
|
if _, _, err := root.Find([]string{"robot", name}); err != nil {
|
|
t.Fatalf("devapp robot %s missing: %v", name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDevAppRobotConnectValidation covers the credential / channel guards on the
|
|
// dry-run path (which never launches the Stream connector).
|
|
func TestDevAppRobotConnectValidation(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
wantErr string
|
|
wantJSON []string // substrings expected in successful dry-run output
|
|
wantStderr []string // substrings expected in combined stderr/stdout stream
|
|
notStderr []string // substrings that must NOT appear
|
|
}{
|
|
{
|
|
name: "no credentials and no unified-app-id",
|
|
args: []string{"--channel", "claudecode", "--dry-run"},
|
|
wantErr: "需要 --robot-client-id/--robot-client-secret",
|
|
},
|
|
{
|
|
name: "unknown channel",
|
|
args: []string{"--channel", "nope", "--robot-client-id", "a", "--robot-client-secret", "b", "--dry-run"},
|
|
wantErr: "未知渠道",
|
|
},
|
|
{
|
|
name: "explicit credentials dry-run emits plan and secret warning",
|
|
args: []string{"--channel", "claudecode", "--robot-client-id", "id1", "--robot-client-secret", "sec1", "--dry-run"},
|
|
wantJSON: []string{"\"credentialSource\"", "flag:--robot-client-id/--robot-client-secret", "stream-bridge", "\"clientId\"", "\"completionState\": \"LOCAL_DEBUG_ONLY\"", "\"doesNotPublish\": true", "\"scope\": \"local_debug_only\""},
|
|
wantStderr: []string{"[connect] WARNING", "--robot-client-secret 出现在命令行", "建议改用 --unified-app-id"},
|
|
},
|
|
{
|
|
name: "unified-app-id dry-run skips credentials get and emits no secret warning",
|
|
args: []string{"--channel", "qoderwork", "--unified-app-id", "UAID", "--dry-run"},
|
|
wantJSON: []string{"credentials get, skipped in dry-run", "\"unifiedAppId\"", "\"completionState\": \"LOCAL_DEBUG_ONLY\"", "\"doesNotPublish\": true", "\"scope\": \"local_debug_only\""},
|
|
notStderr: []string{"[connect] WARNING"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
root := newDevAppTestRoot(&captureRunner{})
|
|
var out bytes.Buffer
|
|
root.SetOut(&out)
|
|
root.SetErr(&out)
|
|
root.SetArgs(append([]string{"dev", "connect"}, tc.args...))
|
|
err := root.Execute()
|
|
|
|
if tc.wantErr != "" {
|
|
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
|
t.Fatalf("want error containing %q, got err=%v out=%s", tc.wantErr, err, out.String())
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v\noutput:\n%s", err, out.String())
|
|
}
|
|
for _, sub := range tc.wantJSON {
|
|
if !strings.Contains(out.String(), sub) {
|
|
t.Fatalf("output missing %q:\n%s", sub, out.String())
|
|
}
|
|
}
|
|
for _, sub := range tc.wantStderr {
|
|
if !strings.Contains(out.String(), sub) {
|
|
t.Fatalf("stderr/stdout missing %q:\n%s", sub, out.String())
|
|
}
|
|
}
|
|
for _, sub := range tc.notStderr {
|
|
if strings.Contains(out.String(), sub) {
|
|
t.Fatalf("stderr/stdout unexpectedly contains %q:\n%s", sub, out.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDevAppRobotConnectLocalDebugNotice(t *testing.T) {
|
|
notice := connectLocalDebugNotice()
|
|
for _, sub := range []string{"本地调试", "不代表线上发布完成", "不会提交版本发布", "APPROVAL_REQUIRED"} {
|
|
if !strings.Contains(notice, sub) {
|
|
t.Fatalf("notice = %q, want substring %q", notice, sub)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDevAppFetchCredentials checks that --unified-app-id credential resolution
|
|
// reuses get_dev_app_credentials and extracts clientId/clientSecret from the
|
|
// MCP envelope.
|
|
func TestDevAppFetchCredentials(t *testing.T) {
|
|
runner := &devAppResponseRunner{response: map[string]any{
|
|
"content": map[string]any{
|
|
"result": map[string]any{
|
|
"clientId": "ck-123",
|
|
"clientSecret": "cs-456",
|
|
},
|
|
},
|
|
}}
|
|
root := newDevAppTestRoot(runner)
|
|
connectCmd, _, err := root.Find([]string{"dev", "connect"})
|
|
if err != nil {
|
|
t.Fatalf("find connect: %v", err)
|
|
}
|
|
connectCmd.SetContext(context.Background())
|
|
|
|
id, secret, err := devAppFetchCredentials(runner, connectCmd, "UAID-1")
|
|
if err != nil {
|
|
t.Fatalf("devAppFetchCredentials error: %v", err)
|
|
}
|
|
if id != "ck-123" || secret != "cs-456" {
|
|
t.Fatalf("got id=%q secret=%q, want ck-123/cs-456", id, secret)
|
|
}
|
|
if runner.last.Tool != "get_dev_app_credentials" {
|
|
t.Fatalf("Tool = %q, want get_dev_app_credentials", runner.last.Tool)
|
|
}
|
|
if runner.last.CanonicalProduct != devAppProduct {
|
|
t.Fatalf("CanonicalProduct = %q, want %q", runner.last.CanonicalProduct, devAppProduct)
|
|
}
|
|
if got := runner.last.Params["unifiedAppId"]; got != "UAID-1" {
|
|
t.Fatalf("unifiedAppId param = %v, want UAID-1", got)
|
|
}
|
|
}
|
|
|
|
// TestDevAppConnectUnwrap covers the envelope descent and the appKey/appSecret
|
|
// field fallbacks.
|
|
func TestDevAppConnectUnwrap(t *testing.T) {
|
|
// appKey/appSecret fallback when clientId/clientSecret absent.
|
|
payload := devAppConnectUnwrap(map[string]any{
|
|
"result": map[string]any{"appKey": "ak", "appSecret": "as"},
|
|
})
|
|
if got := devAppConnectFirst(payload, "clientId", "appKey"); got != "ak" {
|
|
t.Fatalf("clientId fallback = %q, want ak", got)
|
|
}
|
|
if got := devAppConnectFirst(payload, "clientSecret", "appSecret"); got != "as" {
|
|
t.Fatalf("clientSecret fallback = %q, want as", got)
|
|
}
|
|
// nil / missing tolerated.
|
|
if got := devAppConnectFirst(nil, "clientId"); got != "" {
|
|
t.Fatalf("nil map = %q, want empty", got)
|
|
}
|
|
}
|