Skip to content

Commit 52e0129

Browse files
feat(drive): add quick mode to status diff (#870)
1 parent 8a8dff4 commit 52e0129

14 files changed

Lines changed: 848 additions & 39 deletions

cmd/auth/login.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ func collectScopesForDomains(domains []string, identity string) []string {
507507
// 3. Shortcut scopes matching by Service (only include shortcuts supporting the identity)
508508
for _, sc := range shortcuts.AllShortcuts() {
509509
if domainSet[sc.Service] && shortcutSupportsIdentity(sc, identity) {
510-
for _, s := range sc.ScopesForIdentity(identity) {
510+
for _, s := range sc.DeclaredScopesForIdentity(identity) {
511511
scopeSet[s] = true
512512
}
513513
}

cmd/diagnose_scope_test.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func diagBuild(domains []string) diagOutput {
9797
if sc.Service != domain || !diagShortcutSupportsIdentity(&sc, identity) {
9898
continue
9999
}
100-
for _, scope := range sc.ScopesForIdentity(identity) {
100+
for _, scope := range sc.DeclaredScopesForIdentity(identity) {
101101
k := methodKey{domain, "shortcut", sc.Command, scope}
102102
if e, ok := merged[k]; ok {
103103
e.Identity = appendUniq(e.Identity, identity)
@@ -169,6 +169,25 @@ func appendUniq(ss []string, s string) []string {
169169
return append(ss, s)
170170
}
171171

172+
func TestDiagBuild_ShortcutIncludesConditionalScopes(t *testing.T) {
173+
out := diagBuild([]string{"drive"})
174+
var sawMetadata, sawDownload bool
175+
for _, method := range out.Methods {
176+
if method.Domain != "drive" || method.Type != "shortcut" || method.Method != "+status" {
177+
continue
178+
}
179+
if method.Scope == "drive:drive.metadata:readonly" {
180+
sawMetadata = true
181+
}
182+
if method.Scope == "drive:file:download" {
183+
sawDownload = true
184+
}
185+
}
186+
if !sawMetadata || !sawDownload {
187+
t.Fatalf("drive +status should advertise both metadata and conditional download scopes, saw metadata=%v download=%v", sawMetadata, sawDownload)
188+
}
189+
}
190+
172191
// ── Snapshot generation ───────────────────────────────────────────────
173192
//
174193
// Generates a JSON snapshot of all API methods and shortcuts with their

cmd/error_auth_hint.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func resolveDeclaredShortcutScopes(cmd *cobra.Command, identity string) []string
7575
if sc.Service != service || sc.Command != cmd.Name() || !shortcutSupportsIdentity(sc, identity) {
7676
continue
7777
}
78-
scopes := sc.ScopesForIdentity(identity)
78+
scopes := sc.DeclaredScopesForIdentity(identity)
7979
if len(scopes) == 0 {
8080
return nil
8181
}

cmd/root_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,32 @@ func TestEnrichMissingScopeError_ShortcutUsesDeclaredScopesWhenNoUAT(t *testing.
284284
}
285285
}
286286

287+
func TestEnrichMissingScopeError_ShortcutIncludesConditionalScopes(t *testing.T) {
288+
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
289+
290+
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
291+
AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
292+
})
293+
f.ResolvedIdentity = core.AsUser
294+
295+
root := &cobra.Command{Use: "lark-cli"}
296+
serviceCmd := &cobra.Command{Use: "drive"}
297+
shortcutCmd := &cobra.Command{Use: "+status"}
298+
root.AddCommand(serviceCmd)
299+
serviceCmd.AddCommand(shortcutCmd)
300+
f.CurrentCommand = shortcutCmd
301+
302+
exitErr := output.ErrNetwork("API call failed: %s", &internalauth.NeedAuthorizationError{})
303+
enrichMissingScopeError(f, exitErr)
304+
305+
if exitErr.Detail == nil {
306+
t.Fatal("expected error detail")
307+
}
308+
if !strings.Contains(exitErr.Detail.Hint, "current command requires scope(s): drive:drive.metadata:readonly, drive:file:download") {
309+
t.Fatalf("expected conditional scope hint for drive +status, got %q", exitErr.Detail.Hint)
310+
}
311+
}
312+
287313
func TestEnrichMissingScopeError_AppendsExistingHint(t *testing.T) {
288314
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
289315

shortcuts/common/types.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,18 @@ type Shortcut struct {
3333
Command string
3434
Description string
3535
Risk string // "read" | "write" | "high-risk-write" (empty defaults to "read")
36-
Scopes []string // default scopes (fallback when UserScopes/BotScopes are empty)
37-
UserScopes []string // optional: user-identity scopes (overrides Scopes when non-empty)
38-
BotScopes []string // optional: bot-identity scopes (overrides Scopes when non-empty)
36+
Scopes []string // unconditional pre-flight scopes (fallback when UserScopes/BotScopes are empty)
37+
UserScopes []string // optional: user-identity unconditional scopes (overrides Scopes when non-empty)
38+
BotScopes []string // optional: bot-identity unconditional scopes (overrides Scopes when non-empty)
39+
40+
// ConditionalScopes are additional scopes that only some execution paths
41+
// need (for example a default mode vs. a lighter --quick mode, or a
42+
// destructive flag like --delete-remote). They are surfaced in metadata,
43+
// auth hints, and scope-diagnosis output via DeclaredScopesForIdentity, but
44+
// they are NOT enforced by the framework's unconditional pre-flight check.
45+
ConditionalScopes []string // fallback when ConditionalUserScopes/BotScopes are empty
46+
ConditionalUserScopes []string // optional: user-identity conditional scopes
47+
ConditionalBotScopes []string // optional: bot-identity conditional scopes
3948

4049
// Declarative fields (new framework).
4150
AuthTypes []string // supported identities: "user", "bot" (default: ["user"])
@@ -72,3 +81,47 @@ func (s *Shortcut) ScopesForIdentity(identity string) []string {
7281
}
7382
return s.Scopes
7483
}
84+
85+
// ConditionalScopesForIdentity returns additional flag/path-dependent scopes
86+
// for the given identity. Identity-specific conditional scopes override the
87+
// default ConditionalScopes when present.
88+
func (s *Shortcut) ConditionalScopesForIdentity(identity string) []string {
89+
switch identity {
90+
case "user":
91+
if len(s.ConditionalUserScopes) > 0 {
92+
return s.ConditionalUserScopes
93+
}
94+
case "bot":
95+
if len(s.ConditionalBotScopes) > 0 {
96+
return s.ConditionalBotScopes
97+
}
98+
}
99+
return s.ConditionalScopes
100+
}
101+
102+
// DeclaredScopesForIdentity returns the full scope set agents/help/diagnostics
103+
// should know about for this shortcut: unconditional pre-flight scopes plus
104+
// any conditional scopes that some execution paths may require.
105+
func (s *Shortcut) DeclaredScopesForIdentity(identity string) []string {
106+
base := s.ScopesForIdentity(identity)
107+
extra := s.ConditionalScopesForIdentity(identity)
108+
if len(base) == 0 && len(extra) == 0 {
109+
return nil
110+
}
111+
out := make([]string, 0, len(base)+len(extra))
112+
seen := make(map[string]struct{}, len(base)+len(extra))
113+
for _, scope := range append(base, extra...) {
114+
if scope == "" {
115+
continue
116+
}
117+
if _, ok := seen[scope]; ok {
118+
continue
119+
}
120+
seen[scope] = struct{}{}
121+
out = append(out, scope)
122+
}
123+
if len(out) == 0 {
124+
return nil
125+
}
126+
return out
127+
}

shortcuts/common/types_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,37 @@ func TestScopesForIdentity_NilScopes(t *testing.T) {
7171
t.Errorf("expected nil, got %v", got)
7272
}
7373
}
74+
75+
func TestConditionalScopesForIdentity_FallbackAndOverrides(t *testing.T) {
76+
s := Shortcut{
77+
ConditionalScopes: []string{"c-default"},
78+
ConditionalUserScopes: []string{"c-user"},
79+
ConditionalBotScopes: []string{"c-bot"},
80+
}
81+
if got := s.ConditionalScopesForIdentity("user"); !reflect.DeepEqual(got, []string{"c-user"}) {
82+
t.Errorf("expected user conditional scopes, got %v", got)
83+
}
84+
if got := s.ConditionalScopesForIdentity("bot"); !reflect.DeepEqual(got, []string{"c-bot"}) {
85+
t.Errorf("expected bot conditional scopes, got %v", got)
86+
}
87+
if got := s.ConditionalScopesForIdentity("tenant"); !reflect.DeepEqual(got, []string{"c-default"}) {
88+
t.Errorf("expected default conditional scopes for unknown identity, got %v", got)
89+
}
90+
}
91+
92+
func TestDeclaredScopesForIdentity_MergesAndDeduplicates(t *testing.T) {
93+
s := Shortcut{
94+
Scopes: []string{"base-a", "shared"},
95+
ConditionalScopes: []string{"shared", "cond-b"},
96+
}
97+
if got := s.DeclaredScopesForIdentity("user"); !reflect.DeepEqual(got, []string{"base-a", "shared", "cond-b"}) {
98+
t.Errorf("expected merged declared scopes, got %v", got)
99+
}
100+
}
101+
102+
func TestDeclaredScopesForIdentity_ConditionalOnly(t *testing.T) {
103+
s := Shortcut{ConditionalScopes: []string{"cond-only"}}
104+
if got := s.DeclaredScopesForIdentity("user"); !reflect.DeepEqual(got, []string{"cond-only"}) {
105+
t.Errorf("expected conditional-only declared scopes, got %v", got)
106+
}
107+
}

0 commit comments

Comments
 (0)