Skip to content

feat(shortcuts): introduce TypedShortcut framework - #1110

Open
sang-neo03 wants to merge 14 commits into
mainfrom
feat/shortcut-protocol
Open

feat(shortcuts): introduce TypedShortcut framework#1110
sang-neo03 wants to merge 14 commits into
mainfrom
feat/shortcut-protocol

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces a strongly-typed shortcut protocol that coexists with the legacy common.Shortcut. A generic TypedShortcut[T] outer wrapper, a reflect-driven binder, six typed primitives, and a sectioned --help renderer give shortcut authors compile-time argument types and structured error envelopes (shortcut_oneof_missing / shortcut_oneof_multiple / shortcut_group_incomplete) without forking the existing runShortcut pipeline.

Note

Update (ccf654d): the im +messages-send pilot was reverted. The pilot
was an exploratory end-to-end validation of the framework only. It has been
rolled back so this PR lands the framework aloneim +messages-send
stays on the legacy common.Shortcut path, identical to main. The
framework retains no production caller yet; the first real migration will
come in a follow-up PR. See the "Net result after revert" section below for
the current diff. The Changes / Test Plan sections below describe the
original pilot commits (a07239b / ad4368e) and are kept for history.

Net result after revert (current PR diff)

  • Kept: shortcuts/common/ framework (protocol / binder / typed_shortcut / typed_help / runner / types), shortcuts/common/argstype/ primitives, errs shortcut subtypes, cmd/auth + register.go ShortcutDescriptor / Mountable dual-track dispatch.
  • Reverted: shortcuts/im/ restored to its pre-pilot (main) state — im_messages_send.go back to legacy common.Shortcut; protocol.go / protocol_test.go / im_messages_send_test.go deleted; shortcuts.go no longer exports TypedShortcuts().
  • Removed wiring: addTyped(im.TypedShortcuts()) and the now-unused addTyped helper dropped from register.go; framework doc comments that referenced the pilot types (MessageTarget / MessageContent / VideoContent / RawContent) rewritten as neutral descriptions.
  • Verified: go build ./... and go test ./shortcuts/... ./errs/... ./cmd/auth/... ./cmd/ all pass; gofmt clean; no dangling references to the removed pilot types remain in the framework.

Changes (original pilot — see Update note above)

  • Add shortcuts/common/protocol.goMountable / ShortcutDescriptor / OneOfMarker / Validatable / Normalizable[T] / ArgsValidator / Maybe[T] / HelpExample
  • Add shortcuts/common/binder.go — reflect Args walker with intra-Args tag-uniqueness panic, cobra flag registration, value binding, bindBuckets for nested OneOf / group sub-structs, runNormalize (reflect-dispatched, generic-safe), runValidateValue (recurses into buckets / groups), runFrameworkRules (required / enum / OneOf / group, recursive)
  • Add shortcuts/common/typed_shortcut.goTypedShortcut[T] with 8 metadata methods + mountTyped adapter that synthesizes a legacy shell and reuses the existing identity / scopes / @file / stdin / jq / dry-run / high-risk gate
  • Add shortcuts/common/typed_help.go — sectioned --help (CHOOSE ONE <FIELD> / OPTIONAL / EXAMPLES / GLOBAL FLAGS) with Risk: / Tips: passthrough
  • Add shortcuts/common/argstype/ — 6 typed primitives: ChatID (oc_ prefix), UserOpenID (ou_ prefix), UserOpenIDList (CSV), SafePath (cwd-relative, rejects abs / ..), MediaInput (URL / key / path tri-state), SpreadsheetRef (URL → token Normalize)
  • Add errs/subtypes_shortcut.goSubtypeShortcutOneOfMissing / SubtypeShortcutOneOfMultiple / SubtypeShortcutGroupIncomplete
  • Modify shortcuts/common/types.go — 5 Get* accessors on *Shortcut (GetService / GetCommand / GetDescription / GetAuthTypes / GetRisk)
  • Modify shortcuts/common/runner.gotypedArgs any lifecycle slot + TypedArgs / SetTypedArgs accessors on RuntimeContext
  • Modify shortcuts/register.goAllShortcuts() returns []ShortcutDescriptor; RegisterShortcutsWithContext dispatches Mountable so legacy and typed shortcuts share one path; addLegacy helper boxes each domain's contributions (the addTyped helper and im pilot wiring were later reverted — see Update note)
  • Add shortcuts/im/protocol.goMessageTarget / MessageContent / VideoContent / RawContent (reverted)
  • Modify shortcuts/im/im_messages_send.go — migrated to TypedShortcut[*ImMessagesSendArgs] (reverted; back to legacy)
  • Modify shortcuts/im/shortcuts.go — new TypedShortcuts() exporter (reverted)
  • Modify cmd/auth/login.go, cmd/auth/login_interactive.go, cmd/auth/login_test.go, cmd/error_auth_hint.go, cmd/diagnose_scope_test.go, shortcuts/register_test.go — read shortcuts through ShortcutDescriptor interface methods (GetService / GetCommand / GetAuthTypes / DeclaredScopesForIdentity); shortcutSupportsIdentity accepts the interface

Test Plan

  • go build ./... and unit tests pass (go test ./shortcuts/... ./errs/... ./cmd/auth/... ./cmd/)
  • validate passed (build / vet / unit / convention)
  • gofmt clean; no dangling references to reverted pilot types
  • independent Codex review of the revert: confirmed complete & correct, no dead-code / unused-lint hazard from retaining the framework
  • Note: pilot-specific E2E / acceptance / skill-eval from the original pilot no longer apply, since im +messages-send reverts to its existing (already-tested) legacy behavior.

Related Issues

N/A

Summary by CodeRabbit

  • New Features

    • Typed CLI shortcut framework: structured arg binding, richer typed help, and typed shortcut mounting.
    • New argument validators: safe relative paths, media inputs (URL/key/path), chat IDs, user IDs/lists, and spreadsheet references.
    • IM message-send command migrated to typed args with clearer validation and behavior.
  • Bug Fixes

    • Improved auth-domain scoping and matching to reduce incorrect domain inclusion.
  • Tests

    • Expanded unit tests and new error subtypes covering validators, binder, typed shortcuts, and IM command.

Review Change Stack

@github-actions github-actions Bot added domain/im PR touches the im domain size/XL Architecture-level or global-impact change labels May 26, 2026
@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a typed-shortcut protocol and reflection binder, many arg-type validators, RuntimeContext typed-args storage, typed help, migrates im +messages-send to TypedShortcut, refactors registration to descriptor/mountable, and updates auth/scope call sites to use descriptor accessors.

Changes

Typed Shortcut Framework & ImMessagesSend Migration

Layer / File(s) Summary
Protocol & interfaces
shortcuts/common/protocol.go, shortcuts/common/protocol_test.go
Introduces ShortcutDescriptor, Mountable, OneOfMarker, Validatable, Normalizable[T], ArgsValidator, Maybe[T], and HelpExample.
Argument type validators
shortcuts/common/argstype/*
Adds ChatID, UserOpenID, UserOpenIDList, SpreadsheetRef, SafePath, and MediaInput with Normalize/ValidateValue implementations and tests.
Reflection binder & rules
shortcuts/common/binder.go, shortcuts/common/binder_test.go, shortcuts/common/binder_coverage_test.go
Implements fieldSpec, walkArgs, flag registration/binding, runNormalize, runValidateValue, and framework rules (checkOneOf, checkGroup, checkEnumAndRequired) with tests asserting one-of/group validation subtypes and coverage tests.
TypedShortcut and typed help
shortcuts/common/typed_shortcut.go, shortcuts/common/typed_shortcut_test.go, shortcuts/common/typed_help.go, shortcuts/common/typed_help_test.go
Adds generic TypedShortcut[T] that mounts via a legacy shell delegating to typed hooks, and buildTypedHelp rendering CHOOSE ONE / OPTIONAL / EXAMPLES / GLOBAL FLAGS.
RuntimeContext typed args & legacy getters
shortcuts/common/runner.go, shortcuts/common/runner_args_test.go, shortcuts/common/types.go, shortcuts/common/types_test.go
Adds typedArgs storage with SetTypedArgs/TypedArgs and pointer-receiver getters on *Shortcut for descriptor compatibility.
IM protocol types
shortcuts/im/protocol.go, shortcuts/im/protocol_test.go
Defines MessageTarget and MessageContent one-of variants, VideoContent, and RawContent with JSON validation.
ImMessagesSend migration
shortcuts/im/im_messages_send.go, shortcuts/im/im_messages_send_test.go, shortcuts/im/builders_test.go
Migrates ImMessagesSend to TypedShortcut[*ImMessagesSendArgs], adds bindMessagesSendArgs, cross-field validators (validateVideoGroup, validateMsgTypeInterplay), updates DryRun/Execute to consume typed args, and adds binding/validation/mounting tests.
IM helpers & export refactor
shortcuts/im/helpers.go, shortcuts/im/helpers_test.go, shortcuts/im/shortcuts.go
Adds media-path validation helpers, removes ImMessagesSend from legacy Shortcuts(), adds TypedShortcuts() and updates tests to collect commands from both lists.
Shortcut registration refactor
shortcuts/register.go, shortcuts/register_test.go
Changes AllShortcuts() to return []common.ShortcutDescriptor, builds a combined descriptor list of legacy pointer shortcuts and typed mountables, groups by service, and mounts via MountWithContext; updates tests/JSON generation.
Auth and scope callers updated
cmd/auth/*, cmd/diagnose_scope_test.go, cmd/error_auth_hint.go, cmd/auth/login_test.go
Replaces direct Shortcut field access with descriptor getters (GetService, GetCommand, GetAuthTypes) and updates helper signatures to accept ShortcutDescriptor.
Shortcut validation subtypes
errs/subtypes_shortcut.go, errs/subtypes_shortcut_test.go
Adds SubtypeShortcutOneOfMissing, SubtypeShortcutOneOfMultiple, and SubtypeShortcutGroupIncomplete and tests asserting their string values.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • larksuite/cli#1002: Related changes to shortcut registration/descriptor handling and registry population.
  • larksuite/cli#984: Related auth-hinting and error hint rewrite touching similar auth/error paths.
  • larksuite/cli#322: Related IM media/file handling changes and FileIO abstraction updates.

Suggested reviewers

  • liangshuo-1
  • evandance
  • wittam-01

"🐇 I hop through flags with gentle paws,
Normalize trims and binder draws.
OneOfs pick, groups must be complete,
Typed args stored where runtimes meet.
Tests cheer loud — a carrot treat!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title 'feat(shortcuts): introduce TypedShortcut framework' accurately summarizes the main objective of this PR—introducing a new typed shortcut framework alongside the legacy system.
Description check ✅ Passed The PR description provides a comprehensive summary, detailed changes, test plan with checkmarks, and includes a notable update section clarifying the pilot revert. However, it lacks a 'Related Issues' section with issue links and doesn't strictly follow the provided template structure (particularly the 'Changes' list format).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shortcut-protocol

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@5cd34b1f701828064b9162c35ddcbbdf61cd46a9

🧩 Skill update

npx skills add larksuite/cli#feat/shortcut-protocol -y -g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (3)
shortcuts/register.go (1)

126-135: ⚡ Quick win

Add a duplicate shortcut key guard before mounting.

Right now, duplicate service/command entries across legacy + typed lists are appended silently. A small guard here prevents accidental double-mount regressions.

Suggested patch
 	byService := make(map[string][]common.Mountable)
+	seen := make(map[string]struct{})
 	for _, d := range allShortcuts {
 		m, ok := d.(common.Mountable)
 		if !ok {
 			panic(fmt.Sprintf("shortcut %s/%s missing Mountable", d.GetService(), d.GetCommand()))
 		}
+		key := d.GetService() + "/" + d.GetCommand()
+		if _, exists := seen[key]; exists {
+			panic(fmt.Sprintf("duplicate shortcut registration: %s", key))
+		}
+		seen[key] = struct{}{}
 		byService[d.GetService()] = append(byService[d.GetService()], m)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/register.go` around lines 126 - 135, There is no duplicate-key
guard when building byService from allShortcuts, so identical service/command
pairs can be silently appended; add a seen set (map[string]struct{}) keyed by
fmt.Sprintf("%s/%s", d.GetService(), d.GetCommand()) inside the loop that
iterates allShortcuts and before appending to byService check the seen map and
panic or return an error/log a fatal if the key already exists to prevent
double-mounts; update code around the byService creation, the loop over
allShortcuts, and use the common.Mountable assertion (m, ok :=
d.(common.Mountable)) together with the seen check to locate the duplicate and
stop execution.
shortcuts/im/im_messages_send_test.go (1)

85-86: ⚡ Quick win

Use cmdutil.TestFactory(t, config) in unit tests instead of &cmdutil.Factory{}.

Line 85 and Line 115 instantiate a bare factory directly. Please switch both mount tests to cmdutil.TestFactory(...) to align with the test harness contract.

As per coding guidelines, "**/*_test.go: Use cmdutil.TestFactory(t, config) for test factories in unit tests".

Also applies to: 115-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/im_messages_send_test.go` around lines 85 - 86, Replace the
direct instantiation of cmdutil.Factory (used when mounting the command with
ImMessagesSend.MountWithContext(context.Background(), imParent,
&cmdutil.Factory{})) with the test harness helper cmdutil.TestFactory(t,
config); update both occurrences (the one at the ImMessagesSend.MountWithContext
call and the second mount test near lines 115–116) to call
cmdutil.TestFactory(t, config) so the tests use the proper test factory
contract, passing the current *testing.T and the appropriate test config.
shortcuts/im/builders_test.go (1)

363-418: ⚡ Quick win

Exercise typed-args behavior directly in these ImMessagesSend tests.

Most new calls pass &ImMessagesSendArgs{} and rely on runtime flags, so these cases can still pass even if typed field usage regresses. Prefer constructing meaningful ImMessagesSendArgs per case (or binding once through the framework path) so Validate/DryRun coverage tracks the typed contract.

Also applies to: 705-718

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/builders_test.go` around lines 363 - 418, Tests for
ImMessagesSend call Validate with an empty &ImMessagesSendArgs{} and only
runtime flags, so they don't exercise the typed-args contract; update each
failing subtest to construct and pass a meaningful ImMessagesSendArgs that
matches the scenario (e.g., set Text for the "valid text" case, set Video and
VideoCover for the "video with video-cover" case, set only Video for the "video
without video-cover" case, set only VideoCover for the "video-cover without
video" case, and set MsgType plus Image for the "conflicting explicit msg-type"
case) before calling ImMessagesSend.Validate(context.Background(), args,
runtime), and apply the same change to the similar tests at the other location
(lines ~705-718) so Validate/DryRun exercises the typed fields instead of
relying solely on runtime flags; alternatively bind args via the framework path
once so the typed struct is populated for each test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/common/argstype/safe_path.go`:
- Around line 35-36: The current check uses filepath.Clean(s) which can hide
parent-directory segments (e.g. "a/../b") — first scan the raw input string s
for any literal ".." path segments and reject if found before calling
filepath.Clean; implement this by splitting s on both '/' and '\' (or use
strings.FieldsFunc) and returning an error if any segment == "..", then continue
with the existing clean := filepath.Clean(s) and the remaining checks; reference
the existing variable clean and the use of filepath.Clean in safe_path.go when
making the change.

In `@shortcuts/common/argstype/spreadsheet_ref.go`:
- Around line 28-31: The URL token extraction currently checks segments from
strings.Split(trimmed, "/") and accepts segments that start with "shtcn" even
when they include query or fragment suffixes (e.g., "shtcn...?..."). Update the
extraction logic to strip any query ('?') or fragment ('#') portion from each
segment before checking HasPrefix and before returning SpreadsheetRef; do the
same fix in the other loop/block referenced around the SheetRef validation (the
code that returns SpreadsheetRef(seg) and the similar check at lines ~55-61).
Ensure you sanitize each seg by cutting at the first '?' or '#' (or parse the
URL and use the path segments) so only the raw token is validated and returned.

In `@shortcuts/common/argstype/user_open_id_list.go`:
- Around line 32-46: In UserOpenIDList.ValidateValue, add an explicit guard to
reject an explicitly empty parsed list (check len(l) == 0) and return an
errs.ValidationError referencing the flagName and a clear message like "empty
user open_id list" so inputs like ",," fail fast; keep the existing per-item
"ou_" prefix checks (function: ValidateValue on type UserOpenIDList, variables:
l and flagName, helper itoa) and ensure the new empty-list error uses the same
errs.Problem Category/Subtype pattern as the other errors.

In `@shortcuts/common/binder.go`:
- Around line 308-320: The code always calls cmd.Flags().GetString for both
pointer and non-pointer fields (s.FlagName) and then reflect.Converts that
string into the target type, which breaks for non-string leaves (bool/int) and
can panic; change the binding in binder.go (the s.IsPtr branch and the
non-pointer branch that use fv and elemType) to detect the target kind (use
elemType := fv.Type().Elem() for pointers and target := fv.Type() for
non-pointers) and switch on kind to call the appropriate flag getter (GetBool,
GetInt/GetInt64/GetUint, GetFloat64, GetStringSlice, etc.), handle errors from
those getters, and then set the reflect.Value using
reflect.ValueOf(parsedValue).Convert(target) (or set a new pointer when s.IsPtr)
so typed leaves are bound with their native types instead of always using
strings.

In `@shortcuts/common/typed_help_test.go`:
- Around line 29-33: In TestTypedHelp_RendersSections, the test must isolate
config state by setting the LARKSUITE_CLI_CONFIG_DIR environment variable; at
the start of the test (inside TestTypedHelp_RendersSections, before creating the
cobra.Command or calling walkArgs/reflect.TypeOf(&helpDemoArgs{})), call
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) so the test uses a temporary
config directory and doesn't depend on ambient state.
- Around line 33-34: The test currently ignores the error returned by
walkArgs(reflect.TypeOf(&helpDemoArgs{})); change this to capture the error
(e.g. specs, err := walkArgs(...)) and fail fast if err != nil by calling the
test helper (e.g. t.Fatalf or t.Fatal) with the error message before calling
buildTypedHelp; keep using the same symbols walkArgs, helpDemoArgs, specs, and
buildTypedHelp so the rest of the test remains unchanged.

In `@shortcuts/common/typed_help.go`:
- Around line 147-156: renderGlobalFlags currently uses fs.VisitAll and appends
every flag (via the loop containing fs.VisitAll and globals = append(...))
without checking for hidden flags, causing flags with f.Hidden==true to appear
in "GLOBAL FLAGS"; update the VisitAll callback to skip flags where f.Hidden is
true (i.e., add an early return when f.Hidden), preserving the existing dedup
logic (checks against rendered and seen) and still appending only non-hidden
flags to the globals slice (refer to fs.VisitAll, the closure's f variable, the
rendered/seen maps, and globalFlag{Name:..., Shorthand:..., Usage:...}).
- Around line 28-29: Change the typed help output to write to stderr and omit
hidden flags when rendering global flags: replace uses of cmd.OutOrStdout() with
cmd.ErrOrStderr() (so human-readable help goes to stderr), and update
renderGlobalFlags (the VisitAll iteration over flags) to skip flags where
f.Hidden is true (e.g., continue if f.Hidden) so hidden flags are not listed
under GLOBAL FLAGS. Ensure both changes reference the existing cmd object and
the renderGlobalFlags/VisitAll flag iteration so behavior and signatures remain
unchanged.

In `@shortcuts/common/typed_shortcut_test.go`:
- Around line 65-66: Replace direct construction of cmdutil.Factory (the
&cmdutil.Factory{} passed into ts.MountWithContext and other test calls) with
the test helper cmdutil.TestFactory(t, config); before creating the factory,
call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) to isolate config state;
create or pass an appropriate minimal config value into cmdutil.TestFactory(t,
config) for these tests (references: ts.MountWithContext, cmdutil.Factory,
cmdutil.TestFactory, t.Setenv, "LARKSUITE_CLI_CONFIG_DIR").

In `@shortcuts/common/typed_shortcut.go`:
- Around line 189-192: The anonymous Execute wrapper currently calls s.Execute
unconditionally which can panic if s.Execute is nil; update the wrapper (the
Execute func that receives context.Context and *RuntimeContext and calls
rt.TypedArgs()) to first check if s.Execute == nil and return a clear error
(e.g., using fmt.Errorf or errors.New) when nil, otherwise invoke s.Execute(c,
args, rt) as before so missing handlers fail gracefully instead of panicking.

In `@shortcuts/im/helpers.go`:
- Around line 88-94: validateMediaFlagPath currently ignores os.IsNotExist
errors so missing local files pass validation; update the fio.Stat error
handling in validateMediaFlagPath to treat any non-nil err (including
os.IsNotExist) as a validation failure by returning output.ErrValidation("%s:
%v", flagName, err) when err != nil, leaving the early-returns for
empty/http/media-key values unchanged.

In `@shortcuts/register_test.go`:
- Around line 82-90: The test for AllShortcuts currently only compares lengths
after appending and can pass if slices alias; change it to capture the original
slice (orig := AllShortcuts()), then call got := AllShortcuts(), append to got,
and assert that orig's length and element values are unchanged by the append
(e.g., len(orig) == len(AllShortcuts()) and elements of orig equal elements of
AllShortcuts()); use the AllShortcuts symbol and the test function in
shortcuts/register_test.go to locate and replace the existing
length-after-append check so the test proves the returned slice is a true
defensive copy rather than an alias.

---

Nitpick comments:
In `@shortcuts/im/builders_test.go`:
- Around line 363-418: Tests for ImMessagesSend call Validate with an empty
&ImMessagesSendArgs{} and only runtime flags, so they don't exercise the
typed-args contract; update each failing subtest to construct and pass a
meaningful ImMessagesSendArgs that matches the scenario (e.g., set Text for the
"valid text" case, set Video and VideoCover for the "video with video-cover"
case, set only Video for the "video without video-cover" case, set only
VideoCover for the "video-cover without video" case, and set MsgType plus Image
for the "conflicting explicit msg-type" case) before calling
ImMessagesSend.Validate(context.Background(), args, runtime), and apply the same
change to the similar tests at the other location (lines ~705-718) so
Validate/DryRun exercises the typed fields instead of relying solely on runtime
flags; alternatively bind args via the framework path once so the typed struct
is populated for each test.

In `@shortcuts/im/im_messages_send_test.go`:
- Around line 85-86: Replace the direct instantiation of cmdutil.Factory (used
when mounting the command with
ImMessagesSend.MountWithContext(context.Background(), imParent,
&cmdutil.Factory{})) with the test harness helper cmdutil.TestFactory(t,
config); update both occurrences (the one at the ImMessagesSend.MountWithContext
call and the second mount test near lines 115–116) to call
cmdutil.TestFactory(t, config) so the tests use the proper test factory
contract, passing the current *testing.T and the appropriate test config.

In `@shortcuts/register.go`:
- Around line 126-135: There is no duplicate-key guard when building byService
from allShortcuts, so identical service/command pairs can be silently appended;
add a seen set (map[string]struct{}) keyed by fmt.Sprintf("%s/%s",
d.GetService(), d.GetCommand()) inside the loop that iterates allShortcuts and
before appending to byService check the seen map and panic or return an
error/log a fatal if the key already exists to prevent double-mounts; update
code around the byService creation, the loop over allShortcuts, and use the
common.Mountable assertion (m, ok := d.(common.Mountable)) together with the
seen check to locate the duplicate and stop execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 209aa335-569d-4ed7-8c1a-08a780d09bdd

📥 Commits

Reviewing files that changed from the base of the PR and between b783561 and 6d67e5b.

📒 Files selected for processing (42)
  • cmd/auth/login.go
  • cmd/auth/login_interactive.go
  • cmd/auth/login_test.go
  • cmd/diagnose_scope_test.go
  • cmd/error_auth_hint.go
  • errs/subtypes_shortcut.go
  • errs/subtypes_shortcut_test.go
  • shortcuts/common/argstype/chat_id.go
  • shortcuts/common/argstype/chat_id_test.go
  • shortcuts/common/argstype/media_input.go
  • shortcuts/common/argstype/media_input_test.go
  • shortcuts/common/argstype/safe_path.go
  • shortcuts/common/argstype/safe_path_test.go
  • shortcuts/common/argstype/spreadsheet_ref.go
  • shortcuts/common/argstype/spreadsheet_ref_test.go
  • shortcuts/common/argstype/user_open_id.go
  • shortcuts/common/argstype/user_open_id_list.go
  • shortcuts/common/argstype/user_open_id_list_test.go
  • shortcuts/common/argstype/user_open_id_test.go
  • shortcuts/common/binder.go
  • shortcuts/common/binder_test.go
  • shortcuts/common/protocol.go
  • shortcuts/common/protocol_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_args_test.go
  • shortcuts/common/typed_help.go
  • shortcuts/common/typed_help_test.go
  • shortcuts/common/typed_shortcut.go
  • shortcuts/common/typed_shortcut_test.go
  • shortcuts/common/types.go
  • shortcuts/common/types_test.go
  • shortcuts/im/builders_test.go
  • shortcuts/im/helpers.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/im/im_messages_send_test.go
  • shortcuts/im/protocol.go
  • shortcuts/im/protocol_test.go
  • shortcuts/im/shortcuts.go
  • shortcuts/im/validate_media_test.go
  • shortcuts/register.go
  • shortcuts/register_test.go

Comment thread shortcuts/common/argstype/safe_path.go
Comment thread shortcuts/common/argstype/spreadsheet_ref.go
Comment thread shortcuts/common/argstype/user_open_id_list.go Outdated
Comment thread shortcuts/common/binder.go Outdated
Comment thread shortcuts/common/typed_help_test.go Outdated
Comment thread shortcuts/common/typed_help.go
Comment thread shortcuts/common/typed_shortcut_test.go
Comment thread shortcuts/common/typed_shortcut.go
Comment thread shortcuts/im/helpers.go Outdated
Comment thread shortcuts/register_test.go
@sang-neo03
sang-neo03 force-pushed the feat/shortcut-protocol branch from 6d67e5b to bd95794 Compare May 27, 2026 03:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
shortcuts/im/helpers.go (1)

92-93: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fail fast on missing local media paths.

Line 92 currently treats missing files as valid input. This defers a user-input error to a later stage and weakens preflight validation.

Suggested fix
 import (
 	"bytes"
 	"context"
 	"encoding/binary"
 	"encoding/json"
 	"fmt"
 	"io"
 	"math"
 	"net/http"
 	"net/url"
-	"os"
 	"path"
 	"path/filepath"
 	"regexp"
 	"strconv"
 	"strings"
@@
 func validateMediaFlagPath(fio fileio.FileIO, flagName, value string) error {
 	if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || isMediaKey(value) {
 		return nil
 	}
-	if _, err := fio.Stat(value); err != nil && !os.IsNotExist(err) {
+	if _, err := fio.Stat(value); err != nil {
 		return output.ErrValidation("%s: %v", flagName, err)
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/helpers.go` around lines 92 - 93, The preflight validation in
shortcuts/im/helpers.go currently ignores os.IsNotExist errors when statting the
path; change the check in the block that calls fio.Stat(value) (the validation
that returns output.ErrValidation("%s: %v", flagName, err)) to treat any non-nil
err — including os.IsNotExist — as a validation failure so missing local media
paths fail fast; locate the fio.Stat(value) call and remove the special-case
os.IsNotExist(err) condition so the function that returns output.ErrValidation
using flagName and value runs whenever err != nil.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@shortcuts/im/helpers.go`:
- Around line 92-93: The preflight validation in shortcuts/im/helpers.go
currently ignores os.IsNotExist errors when statting the path; change the check
in the block that calls fio.Stat(value) (the validation that returns
output.ErrValidation("%s: %v", flagName, err)) to treat any non-nil err —
including os.IsNotExist — as a validation failure so missing local media paths
fail fast; locate the fio.Stat(value) call and remove the special-case
os.IsNotExist(err) condition so the function that returns output.ErrValidation
using flagName and value runs whenever err != nil.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a01b8809-103e-4b21-a758-f64431a04621

📥 Commits

Reviewing files that changed from the base of the PR and between 6d67e5b and bd95794.

📒 Files selected for processing (42)
  • cmd/auth/login.go
  • cmd/auth/login_interactive.go
  • cmd/auth/login_test.go
  • cmd/diagnose_scope_test.go
  • cmd/error_auth_hint.go
  • errs/subtypes_shortcut.go
  • errs/subtypes_shortcut_test.go
  • shortcuts/common/argstype/chat_id.go
  • shortcuts/common/argstype/chat_id_test.go
  • shortcuts/common/argstype/media_input.go
  • shortcuts/common/argstype/media_input_test.go
  • shortcuts/common/argstype/safe_path.go
  • shortcuts/common/argstype/safe_path_test.go
  • shortcuts/common/argstype/spreadsheet_ref.go
  • shortcuts/common/argstype/spreadsheet_ref_test.go
  • shortcuts/common/argstype/user_open_id.go
  • shortcuts/common/argstype/user_open_id_list.go
  • shortcuts/common/argstype/user_open_id_list_test.go
  • shortcuts/common/argstype/user_open_id_test.go
  • shortcuts/common/binder.go
  • shortcuts/common/binder_test.go
  • shortcuts/common/protocol.go
  • shortcuts/common/protocol_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_args_test.go
  • shortcuts/common/typed_help.go
  • shortcuts/common/typed_help_test.go
  • shortcuts/common/typed_shortcut.go
  • shortcuts/common/typed_shortcut_test.go
  • shortcuts/common/types.go
  • shortcuts/common/types_test.go
  • shortcuts/im/builders_test.go
  • shortcuts/im/helpers.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/im/im_messages_send_test.go
  • shortcuts/im/protocol.go
  • shortcuts/im/protocol_test.go
  • shortcuts/im/shortcuts.go
  • shortcuts/im/validate_media_test.go
  • shortcuts/register.go
  • shortcuts/register_test.go
✅ Files skipped from review due to trivial changes (2)
  • shortcuts/common/argstype/user_open_id.go
  • shortcuts/im/validate_media_test.go
🚧 Files skipped from review as they are similar to previous changes (38)
  • shortcuts/common/types.go
  • errs/subtypes_shortcut.go
  • errs/subtypes_shortcut_test.go
  • shortcuts/common/argstype/safe_path_test.go
  • shortcuts/common/runner_args_test.go
  • shortcuts/common/argstype/spreadsheet_ref_test.go
  • shortcuts/common/argstype/user_open_id_list_test.go
  • shortcuts/common/typed_help_test.go
  • cmd/auth/login_interactive.go
  • cmd/error_auth_hint.go
  • shortcuts/common/argstype/chat_id_test.go
  • shortcuts/common/argstype/user_open_id_test.go
  • shortcuts/common/argstype/spreadsheet_ref.go
  • shortcuts/common/argstype/media_input.go
  • shortcuts/common/argstype/chat_id.go
  • shortcuts/im/shortcuts.go
  • shortcuts/common/types_test.go
  • cmd/auth/login.go
  • cmd/diagnose_scope_test.go
  • shortcuts/common/argstype/user_open_id_list.go
  • shortcuts/im/protocol_test.go
  • shortcuts/common/argstype/safe_path.go
  • shortcuts/im/protocol.go
  • shortcuts/register.go
  • shortcuts/common/protocol.go
  • cmd/auth/login_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/argstype/media_input_test.go
  • shortcuts/im/im_messages_send_test.go
  • shortcuts/common/binder_test.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/builders_test.go
  • shortcuts/common/typed_shortcut.go
  • shortcuts/common/typed_shortcut_test.go
  • shortcuts/common/binder.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/register_test.go
  • shortcuts/common/typed_help.go

@codecov

codecov Bot commented May 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.66368% with 226 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.27%. Comparing base (2bbab4d) to head (5cd34b1).

Files with missing lines Patch % Lines
shortcuts/common/binder.go 73.15% 79 Missing and 41 partials ⚠️
shortcuts/common/typed_shortcut.go 58.40% 41 Missing and 6 partials ⚠️
shortcuts/common/typed_help.go 81.81% 18 Missing and 4 partials ⚠️
shortcuts/common/argstype/spreadsheet_ref.go 55.26% 15 Missing and 2 partials ⚠️
shortcuts/common/argstype/safe_path.go 68.75% 8 Missing and 2 partials ⚠️
shortcuts/common/runner.go 89.47% 2 Missing and 2 partials ⚠️
cmd/auth/login_interactive.go 60.00% 1 Missing and 1 partial ⚠️
shortcuts/common/argstype/media_input.go 83.33% 1 Missing and 1 partial ⚠️
shortcuts/register.go 93.54% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1110      +/-   ##
==========================================
+ Coverage   69.21%   69.27%   +0.05%     
==========================================
  Files         639      647       +8     
  Lines       59794    60615     +821     
==========================================
+ Hits        41389    41992     +603     
- Misses      15059    15221     +162     
- Partials     3346     3402      +56     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sang-neo03
sang-neo03 force-pushed the feat/shortcut-protocol branch from bd95794 to c82ad7b Compare May 27, 2026 03:12
Add strongly-typed shortcut protocol that coexists with the legacy
common.Shortcut. The new common.TypedShortcut[T] is a generic outer
wrapper backed by a reflect-driven binder; both legacy and typed
shortcuts satisfy a new common.Mountable / common.ShortcutDescriptor
interface pair so register.go can dispatch either through the same
pipeline.

Framework (shortcuts/common):
- protocol.go — Mountable / ShortcutDescriptor / OneOfMarker /
  Validatable / Normalizable[T] / ArgsValidator / Maybe[T] /
  HelpExample
- binder.go — reflect Args walk, intra-Args flag-tag uniqueness
  panic, cobra flag registration, bindFlags + bindMaybe, runNormalize
  (via MethodByName dispatch — Normalizable[T] can't be type-asserted
  through a non-generic interface), runValidateValue, runFrameworkRules
  for required / enum / OneOf / group
- typed_shortcut.go — TypedShortcut[T] struct, 8 descriptor methods,
  mountTyped adapter that synthesizes a legacy Shortcut shell and
  reuses runShortcut verbatim (identity / scopes / @file / stdin / jq
  / dry-run / high-risk gate)
- typed_help.go — sectioned --help (CHOOSE ONE / OPTIONAL / EXAMPLES)
  with cmdutil.GetRisk/GetTips passthrough so typed shortcuts keep the
  Risk: and Tips: blocks
- runner.go — typedArgs lifecycle slot on RuntimeContext
- types.go — 5 GetX accessors on *Shortcut so legacy shortcuts
  satisfy ShortcutDescriptor alongside the existing pointer-receiver
  scope methods

Typed primitives (shortcuts/common/argstype):
- ChatID / UserOpenID / UserOpenIDList — prefix-validated identifiers
- SafePath — cwd-relative, rejects absolute paths and ".." segments
- MediaInput — tri-state (URL bypass / img_xxx-file_xxx key bypass /
  SafePath delegation)
- SpreadsheetRef — Normalize extracts shtcn token from feishu URLs

Error contract (errs):
- 3 new Subtype constants: shortcut_oneof_missing /
  shortcut_oneof_multiple / shortcut_group_incomplete. Per-field
  failures (required / enum / typed primitive format) reuse the
  existing SubtypeInvalidArgument so no new error type is introduced.

Registry refactor (shortcuts + cmd):
- AllShortcuts() now returns []common.ShortcutDescriptor; legacy
  shortcuts are boxed as *Shortcut (pointer required for the
  pointer-receiver scope methods), typed shortcuts boxed as Mountable
- Register dispatches via the Mountable interface
- cmd/auth/login, cmd/auth/login_interactive, cmd/error_auth_hint,
  cmd/diagnose_scope_test, shortcuts/register_test (shortcuts.json
  generator) updated to read through GetService / GetCommand /
  GetAuthTypes / GetDescription / DeclaredScopesForIdentity
- shortcutSupportsIdentity helpers accept ShortcutDescriptor

Pilot (shortcuts/im):
- protocol.go — MessageTarget / MessageContent (with seven content
  variants) / VideoContent (paired video + cover) / RawContent
  (--content with explicit msg-type, validates JSON in
  ValidateValue)
- im_messages_send.go — migrated to TypedShortcut[*ImMessagesSendArgs].
  Inline Validate closure is replaced by framework-derived checks
  (OneOf target, OneOf content, VideoContent group, typed-primitive
  formats, RawContent JSON). Helpers (resolveMediaContent,
  wrapMarkdownAsPostForDryRun, normalizeAtMentions, etc.) reused
  verbatim; only the field-access pattern changes from
  runtime.Str("x") to args.X.
- shortcuts.go — new TypedShortcuts() exporter; ImMessagesSend
  removed from the legacy Shortcuts() slice so it is not
  double-mounted
- register.go wires addTyped(im.TypedShortcuts()) into init

Known follow-ups:
- runFrameworkRules and bindFlags do not recurse into OneOf bucket /
  group sub-structs; im messages-send compensates with a local
  bindMessagesSendArgs + validateVideoGroup. Generalizing the binder
  to recurse will let future migrations drop the local shim.
- common.ValidateChatID / common.ValidateUserID become redundant
  once all legacy shortcuts that call them migrate; can be retired
  with the last legacy caller.

Refs: docs/superpowers/specs/2026-05-26-shortcut-protocol-design.md
@sang-neo03
sang-neo03 force-pushed the feat/shortcut-protocol branch from c82ad7b to a07239b Compare May 27, 2026 03:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/im/im_messages_send_test.go`:
- Around line 40-49: The test currently only compares lengths and iterates
values which allows duplicates like ["bot","bot"] to pass; change the assertion
to compare unique auth types by converting ImMessagesSend.GetAuthTypes() into a
map/set (e.g., seen := map[string]bool{}) then ensure every expected key in
wantAuth exists in seen and that seen has no unexpected keys and equal
cardinality to wantAuth; use ImMessagesSend.GetAuthTypes to locate the call and
update the loop/checks to detect duplicates and missing members.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71545eb9-59c3-4571-8da0-3176d56f82db

📥 Commits

Reviewing files that changed from the base of the PR and between c82ad7b and a07239b.

📒 Files selected for processing (42)
  • cmd/auth/login.go
  • cmd/auth/login_interactive.go
  • cmd/auth/login_test.go
  • cmd/diagnose_scope_test.go
  • cmd/error_auth_hint.go
  • errs/subtypes_shortcut.go
  • errs/subtypes_shortcut_test.go
  • shortcuts/common/argstype/chat_id.go
  • shortcuts/common/argstype/chat_id_test.go
  • shortcuts/common/argstype/media_input.go
  • shortcuts/common/argstype/media_input_test.go
  • shortcuts/common/argstype/safe_path.go
  • shortcuts/common/argstype/safe_path_test.go
  • shortcuts/common/argstype/spreadsheet_ref.go
  • shortcuts/common/argstype/spreadsheet_ref_test.go
  • shortcuts/common/argstype/user_open_id.go
  • shortcuts/common/argstype/user_open_id_list.go
  • shortcuts/common/argstype/user_open_id_list_test.go
  • shortcuts/common/argstype/user_open_id_test.go
  • shortcuts/common/binder.go
  • shortcuts/common/binder_test.go
  • shortcuts/common/protocol.go
  • shortcuts/common/protocol_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/runner_args_test.go
  • shortcuts/common/typed_help.go
  • shortcuts/common/typed_help_test.go
  • shortcuts/common/typed_shortcut.go
  • shortcuts/common/typed_shortcut_test.go
  • shortcuts/common/types.go
  • shortcuts/common/types_test.go
  • shortcuts/im/builders_test.go
  • shortcuts/im/helpers.go
  • shortcuts/im/helpers_test.go
  • shortcuts/im/im_messages_send.go
  • shortcuts/im/im_messages_send_test.go
  • shortcuts/im/protocol.go
  • shortcuts/im/protocol_test.go
  • shortcuts/im/shortcuts.go
  • shortcuts/im/validate_media_test.go
  • shortcuts/register.go
  • shortcuts/register_test.go
✅ Files skipped from review due to trivial changes (1)
  • shortcuts/im/validate_media_test.go
🚧 Files skipped from review as they are similar to previous changes (37)
  • shortcuts/common/typed_help_test.go
  • shortcuts/common/argstype/chat_id.go
  • shortcuts/common/runner_args_test.go
  • errs/subtypes_shortcut_test.go
  • cmd/auth/login.go
  • cmd/auth/login_interactive.go
  • shortcuts/common/types_test.go
  • shortcuts/common/argstype/spreadsheet_ref.go
  • shortcuts/im/helpers.go
  • shortcuts/common/runner.go
  • shortcuts/common/argstype/spreadsheet_ref_test.go
  • shortcuts/common/protocol.go
  • shortcuts/common/argstype/media_input_test.go
  • shortcuts/common/argstype/user_open_id_list.go
  • shortcuts/im/builders_test.go
  • shortcuts/common/argstype/user_open_id_test.go
  • cmd/diagnose_scope_test.go
  • errs/subtypes_shortcut.go
  • shortcuts/common/typed_help.go
  • shortcuts/im/protocol.go
  • shortcuts/common/typed_shortcut.go
  • shortcuts/common/argstype/user_open_id.go
  • shortcuts/common/argstype/safe_path.go
  • shortcuts/common/argstype/chat_id_test.go
  • shortcuts/common/protocol_test.go
  • shortcuts/im/protocol_test.go
  • shortcuts/common/typed_shortcut_test.go
  • cmd/error_auth_hint.go
  • shortcuts/common/types.go
  • shortcuts/register.go
  • cmd/auth/login_test.go
  • shortcuts/register_test.go
  • shortcuts/im/helpers_test.go
  • shortcuts/common/binder_test.go
  • shortcuts/common/binder.go
  • shortcuts/common/argstype/media_input.go
  • shortcuts/im/im_messages_send.go

Comment thread shortcuts/im/im_messages_send_test.go Outdated
- im_messages_send_test: assert auth-type set membership (reject
  duplicates / missing members) per CodeRabbit review
- binder_coverage_test: cover bindMaybe, bindBuckets / bindBucketInner,
  bucketLeafValue, runNormalize / asString, checkGroup,
  checkEnumAndRequired, and group recursion in runValidateValue — lifts
  patch coverage over the 60% gate

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
shortcuts/common/binder_coverage_test.go (2)

159-171: ⚡ Quick win

Assert pointer normalization explicitly.

TokenPtr is set up to exercise pointer normalization but never asserted. Add a direct check so this branch is validated, not just executed.

Proposed assertion
 if args.Token != "c:raw" {
   t.Errorf("Token = %q, want c:raw", args.Token)
 }
+if args.TokenPtr == nil || *args.TokenPtr != "c:xy" {
+  t.Errorf("TokenPtr = %v, want c:xy", args.TokenPtr)
+}
 if got := buf.String(); got == "" || !bytes.Contains([]byte(got), []byte("hint: raw")) {
   t.Errorf("stderr = %q, want it to contain the normalize hint", got)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/binder_coverage_test.go` around lines 159 - 171, After
calling runNormalize with bcNormArgs (ptr created by bcNormField("xy")), add an
explicit assertion that the pointer-normalized value was updated: check the
dereferenced TokenPtr on args (e.g., *args.TokenPtr) equals the expected
normalized string ("c:xy") and fail the test with t.Fatalf or t.Errorf if it
does not match; reference bcNormField, bcNormArgs, TokenPtr, runNormalize and
args when locating where to add this assertion.

28-29: ⚡ Quick win

Stop discarding setup errors in tests.

Several tests ignore errors from walkArgs, registerFlags, and ParseFlags. This can mask regressions and make failures harder to diagnose.

Suggested pattern to apply across this file
- specs, _ := walkArgs(reflect.TypeOf(&bcMaybeArgs{}))
- _ = registerFlags(cmd, specs)
- _ = cmd.ParseFlags([]string{"--m-on"})
+ specs, err := walkArgs(reflect.TypeOf(&bcMaybeArgs{}))
+ if err != nil {
+   t.Fatalf("walkArgs: %v", err)
+ }
+ if err := registerFlags(cmd, specs); err != nil {
+   t.Fatalf("registerFlags: %v", err)
+ }
+ if err := cmd.ParseFlags([]string{"--m-on"}); err != nil {
+   t.Fatalf("ParseFlags: %v", err)
+ }

Also applies to: 53-54, 119-120, 161-162, 180-180, 201-204, 217-217, 221-223, 243-243, 247-248, 259-260, 267-268, 286-286

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/binder_coverage_test.go` around lines 28 - 29, The tests are
discarding errors from setup calls (e.g., walkArgs, registerFlags, ParseFlags)
which can hide failures; update each call site (such as the
walkArgs(reflect.TypeOf(&bcMaybeArgs{})) call, registerFlags(cmd, specs), and
any ParseFlags uses) to check the returned error and fail the test immediately
(use t.Fatalf or t.Fatalf-like helper or testify/require.NoError) with a clear
message including the error; ensure you propagate and assert errors for all
occurrences listed so test setup failures cause visible test failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@shortcuts/common/binder_coverage_test.go`:
- Around line 159-171: After calling runNormalize with bcNormArgs (ptr created
by bcNormField("xy")), add an explicit assertion that the pointer-normalized
value was updated: check the dereferenced TokenPtr on args (e.g.,
*args.TokenPtr) equals the expected normalized string ("c:xy") and fail the test
with t.Fatalf or t.Errorf if it does not match; reference bcNormField,
bcNormArgs, TokenPtr, runNormalize and args when locating where to add this
assertion.
- Around line 28-29: The tests are discarding errors from setup calls (e.g.,
walkArgs, registerFlags, ParseFlags) which can hide failures; update each call
site (such as the walkArgs(reflect.TypeOf(&bcMaybeArgs{})) call,
registerFlags(cmd, specs), and any ParseFlags uses) to check the returned error
and fail the test immediately (use t.Fatalf or t.Fatalf-like helper or
testify/require.NoError) with a clear message including the error; ensure you
propagate and assert errors for all occurrences listed so test setup failures
cause visible test failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77a27179-29fe-4765-9b55-186259dd15c4

📥 Commits

Reviewing files that changed from the base of the PR and between a07239b and ad4368e.

📒 Files selected for processing (2)
  • shortcuts/common/binder_coverage_test.go
  • shortcuts/im/im_messages_send_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • shortcuts/im/im_messages_send_test.go

The im +messages-send TypedShortcut pilot was exploratory only; revert it
while retaining the TypedShortcut framework for future migrations.

- shortcuts/im: restored to pre-pilot state (im_messages_send.go back to
  legacy common.Shortcut; deleted protocol.go, protocol_test.go,
  im_messages_send_test.go; shortcuts.go drops TypedShortcuts()).
- shortcuts/register.go: removed addTyped(im.TypedShortcuts()) wiring and
  the now-unused addTyped helper; legacy addLegacy + Mountable dispatch
  retained.
- shortcuts/common, argstype, errs subtypes, cmd/auth adapters: kept.
- framework doc comments: replaced examples referencing the removed pilot
  types (MessageTarget/MessageContent/VideoContent/RawContent) with neutral
  descriptions; noted no typed shortcut is registered today.

Framework now has no production caller. Verified: go build ./... and
go test ./shortcuts/... ./errs/... ./cmd/auth/... ./cmd/ all pass.
@github-actions github-actions Bot removed the domain/im PR touches the im domain label May 27, 2026
@sang-neo03 sang-neo03 changed the title feat: introduce typed shortcut framework + im send pilot feat(shortcuts): introduce TypedShortcut framework May 27, 2026
sang-neo03 added 11 commits May 28, 2026 10:57
After reverting the im pilot in ccf654d the TypedShortcut framework
has no production caller, so the CI deadcode check flagged five
methods as newly unreachable. Resolve without breaking the framework:

- Remove TypedShortcut.Mount — a convenience wrapper over
  MountWithContext with zero callers (production OR test); not part
  of any interface contract.
- Add focused unit tests for the four ShortcutDescriptor methods
  required by the Mountable contract — GetAuthTypes plus
  ScopesForIdentity / ConditionalScopesForIdentity /
  DeclaredScopesForIdentity, with table-driven coverage of the
  user / bot / fallback / dedupe branches.

Locally verified: go run golang.org/x/tools/cmd/deadcode@v0.31.0
-test ./... against HEAD vs origin/main yields an empty diff.
bindFlags and bindBuckets only populated top-level leaves and OneOf
buckets; a top-level group sub-struct (a regular nested struct without
OneOf() marker) had its inner flags registered and group-completeness
checked, but its field values were never written back into the Args
struct — Execute would read empty values from args.Group.X.

Add bindGroups as the top-level counterpart to bindBuckets for IsGroup
fields, and invoke it from mountTyped's Validate closure right after
bindBuckets. Behavior mirrors bindBuckets / bindBucketInner:

- Value-type top-level group: always populated; inner fields receive
  cobra flag values (including defaults).
- Pointer-type top-level group: allocated iff at least one inner flag
  was Changed, so a nil group still signals "user did not engage this
  group" while a non-nil group means "the user opted into it".

Tests cover four cases: value group with explicit flags, value group
with defaults applied, pointer group allocated when an inner flag is
set, pointer group left nil when nothing is set.

Verified: go build ./..., go test ./shortcuts/common/..., and
`go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./...` against
HEAD vs origin/main yields zero new dead code.

This closes the previously-documented limitation that group sub-structs
had to be nested inside an OneOf bucket to actually bind values.
…om any inner flag

The previous checkOneOf required group / bucket variants inside a OneOf
to mark exactly one inner field with `oneof_trigger:"true"`, so the
framework could identify "the flag that signals the variant was selected".
This forces the Args writer to think about an implementation detail and
produces a misleading shortcut_oneof_missing when a user supplies only a
companion field (e.g. --video-cover without --video) — the user actually
attempted that variant, but the framework couldn't see it.

Replace the explicit trigger with implicit detection: any inner flag of a
group / bucket variant that is Changed counts as attempting that variant.
The follow-up checkGroup catches the partial-fill case and surfaces a
shortcut_group_incomplete pointing at the missing flag, which is strictly
more useful than the prior oneof_missing.

Code changes:
- binder.go: drop fieldSpec.OneOfTrig, drop parseFieldSpec's oneof_trigger
  tag handling, rewrite checkOneOf with the inferred-attempt logic, delete
  the isTrigger helper (now inlined and simplified).
- typed_help.go: renderFlagsInBucket flattens all inner flags to the
  parent's indent (no more "trigger vs companion" visual distinction,
  matching the framework's new semantics).
- binder_test.go: add three behavioural tests covering the new contract:
  (1) companion alone surfaces group_incomplete (not oneof_missing),
  (2) full group passes,
  (3) simple variant + group companion both attempted yields oneof_multiple.

Verified: go build ./..., go test ./shortcuts/common/...,
`go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./...` against HEAD
vs origin/main yields zero new dead code.

Net: -1 tag, -1 field on fieldSpec, -1 helper function, simpler writer
ergonomics, more accurate error messages.
CI fast-gate's `gofmt -l .` flagged an alignment issue in contentBucket:
gofmt aligns struct field tags to the longest *named* tag-bearing field
in the group; my hand-written extra spaces in `Text *string   \`flag:"ct"\``
got normalised to a single space.

Behaviour-neutral; tests still pass.
…ortcut.Mount

Three fixes surfaced by a dogfood migration of an existing legacy shortcut
to the TypedShortcut framework.

1. Required flags rendered under OPTIONAL
   The typed help renderer had no notion of required vs optional and lumped
   every top-level leaf into a single OPTIONAL section. Add renderRequiredSection
   that lists fields whose `required` tag is set under a REQUIRED: header; the
   updated renderOptionalSection skips those so each leaf appears in exactly
   one section.

2. Default values silently dropped
   Fields tagged `default:"x"` showed no `(default "x")` suffix, unlike cobra's
   legacy default help. Add a formatLeafLine helper that appends `(default "x")`
   whenever fieldSpec.DefaultValue is non-empty; the REQUIRED, OPTIONAL, and
   CHOOSE ONE bucket renderers all reuse it for consistent information density.

3. TypedShortcut.Mount was missing
   Legacy common.Shortcut exposes BOTH .Mount(parent, factory) and
   .MountWithContext. The framework dropped .Mount in 8d8acb8 to satisfy
   deadcode, but a dogfood migration revealed the asymmetry forces every
   migrating shortcut's tests to also switch to MountWithContext. Restore the
   3-line Mount convenience method (delegates to MountWithContext with a
   background context) and add a unit test exercising it directly — the test
   doubles as API documentation and keeps deadcode happy.

Tests added:
- TestTypedShortcut_Mount: verifies the legacy-shaped Mount(parent, factory)
  call still wires the subcommand
- TestTypedHelp_RequiredSectionAndDefaults: asserts REQUIRED comes before
  OPTIONAL, --limit lands in REQUIRED not OPTIONAL, `(default "20")` appears
  for --page-size, and a plain --verbose carries no default suffix

Verified locally: go build ./..., go test ./shortcuts/common/..., gofmt -l,
and `go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./...` vs
origin/main — all pass with zero new dead code.
…i-state

Maybe[T] solved a real problem (distinguishing "user did not provide this
flag" from "user provided the type's zero value, e.g. --notify=false or
--limit 0"), but it added a framework-specific wrapper that every migrator
had to learn. Two independent dogfood migrations both flagged it as the
guide's biggest cognitive bump.

This change retires Maybe[T] and reuses the existing OneOf pointer
convention at the top level: *T now means "optional with tri-state
semantics — nil iff the user did not set the flag". The rule is the same
whether the *T sits inside a OneOf bucket (Chat *ChatID — variant not
selected) or at the top level of an Args struct (Notify *bool — flag not
given), so users learn one concept instead of two.

Code:
- binder.go: bindLeaf now gates pointer allocation behind
  cmd.Flags().Changed(name); previously a *T top-level pointer was always
  allocated, defeating the "nil = not given" semantic. fieldSpec loses the
  IsMaybe field; parseFieldSpec drops Maybe-shape detection; registerFlags
  / bindFlags drop their IsMaybe branches; the bindMaybe helper is deleted.
- protocol.go: Maybe[T] type removed.
- protocol_test.go: TestMaybe_UnsetVsZero removed.
- binder_coverage_test.go: bindMaybe tests replaced with two
  TestBindLeaf_Ptr* tests covering the new "*T top-level pointer" contract
  — nil when absent, non-nil with the user's value (including the tri-state
  case --notify=false) when explicitly set.

Verified: go build ./..., go test ./shortcuts/common/..., gofmt clean, and
`go run golang.org/x/tools/cmd/deadcode@v0.31.0 -test ./...` vs origin/main
shows zero new dead code.
UserOpenIDList ([]string) had zero production callers and cannot be bound
as a typed flag field: the binder only handles string/bool/int scalars, so
a []string field panics at bind time (reflect.Value.Convert: string cannot
convert to []string). Remove the type, its ParseUserOpenIDList helper and
the test instead of special-casing slice binding for a type nobody uses.
Bring TypedShortcut[T] to parity with legacy common.Shortcut on two flag
capabilities that were silently missing on the typed path:

- @file / stdin: declare `input:"file,stdin"` on an Args field; the binder
  resolves @path (file) and - (stdin) before binding, recursing into OneOf
  buckets and groups. resolveInputForFlag is extracted from the legacy
  resolveInputFlags so both paths behave identically.
- enum: registerLeaf registers shell completion and typed help renders the
  candidate list, matching legacy. enum/input tags on non-string fields now
  error at Mount time instead of being silently skipped at runtime.

Legacy behavior unchanged. Adds binder_input_enum_test.go (11 cases).
Close the last legacy/typed capability gaps so migration never downgrades:

- []string fields: registerLeaf/bindLeaf gain a Slice case. Default maps to
  cobra StringSlice (comma-separated, repeatable); `split:"none"` opts into
  StringArray (repeatable, no comma split). Non-[]string slices and split on
  non-slice fields error at Mount time. bucketLeafValue handles slices in
  OneOf/group too.
- per-flag hidden: `hidden:"true"` tag → fieldSpec.Hidden → MarkHidden;
  typed help skips hidden flags (parity with legacy Flag.Hidden).
- @file/stdin discoverability: typed help now appends a (supports @file /
  - for stdin) hint for input-tagged flags, matching legacy.

Adds binder_slice_hidden_test.go (11 cases).
…unt parity

Two parity edge cases from a follow-up audit:

- []<named string> element types (type ID string; field []ID) passed parse
  validation but panicked at bind via reflect.Convert([]string -> []ID). Build
  the slice element-by-element with SetString (stringsToSlice) so plain
  []string, named slice types and named element types all bind correctly.
- nil Execute: mountTyped now skips mounting (matching legacy
  Shortcut.MountWithContext) instead of mounting a command that errors only at
  invocation, keeping the command tree identical after migration.

Adds binder_namedslice_nilexec_test.go (4 cases).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant