fix(im): pin explicit identity on user-only examples and run them verbatim

This commit is contained in:
luozhixiong
2026-07-17 17:00:08 +08:00
parent 7549ed1ed5
commit a6fd563866
9 changed files with 61 additions and 43 deletions

View File

@@ -36,7 +36,7 @@ var ImFeedGroupListItem = common.Shortcut{
{Name: "end-time", Desc: "update-time window end (Unix milliseconds as a decimal string)"},
},
Tips: []string{
`Example: lark-cli im +feed-group-list-item --feed-group-id <feed_group_id>`,
`Example: lark-cli im +feed-group-list-item --feed-group-id <feed_group_id> --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
return validateFeedGroupListOptions(runtime)

View File

@@ -28,7 +28,7 @@ var ImFeedGroupQueryItem = common.Shortcut{
{Name: "feed-id", Desc: "comma-separated chat IDs (oc_xxx); feed_type is fixed to chat (required)"},
},
Tips: []string{
`Example: lark-cli im +feed-group-query-item --feed-group-id <feed_group_id> --feed-id <chat_id>`,
`Example: lark-cli im +feed-group-query-item --feed-group-id <feed_group_id> --feed-id <chat_id> --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := buildFeedGroupQueryItemBody(runtime)

View File

@@ -35,8 +35,8 @@ var ImFeedShortcutCreate = common.Shortcut{
Desc: "append at the bottom of the shortcut list; mutually exclusive with --head"},
},
Tips: []string{
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id>`,
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --tail`,
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --as user`,
`Example: lark-cli im +feed-shortcut-create --chat-id <chat_id> --tail --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
if _, err := collectChatIDs(runtime); err != nil {

View File

@@ -29,7 +29,7 @@ var ImFeedShortcutRemove = common.Shortcut{
Desc: "open_chat_id to remove from feed shortcuts (oc_xxx); required; repeat the flag or pass comma-separated; max 10 per call"},
},
Tips: []string{
`Example: lark-cli im +feed-shortcut-remove --chat-id <chat_id1>,<chat_id2>`,
`Example: lark-cli im +feed-shortcut-remove --chat-id <chat_id1>,<chat_id2> --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := collectChatIDs(runtime)

View File

@@ -28,7 +28,7 @@ var ImFlagCancel = common.Shortcut{
{Name: "flag-type", Desc: "flag type override: message|feed; omit to double-cancel both layers"},
},
Tips: []string{
`Example: lark-cli im +flag-cancel --message-id <message_id>`,
`Example: lark-cli im +flag-cancel --message-id <message_id> --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, _, err := buildCancelItemsForPreview(runtime)

View File

@@ -27,8 +27,8 @@ var ImFlagCreate = common.Shortcut{
{Name: "flag-type", Desc: "flag type: message (default) or feed"},
},
Tips: []string{
`Example: lark-cli im +flag-create --message-id <message_id>`,
`Example: lark-cli im +flag-create --message-id <message_id> --flag-type feed`,
`Example: lark-cli im +flag-create --message-id <message_id> --as user`,
`Example: lark-cli im +flag-create --message-id <message_id> --flag-type feed --as user`,
},
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
_, err := buildCreateItemForPreview(runtime)

View File

@@ -52,8 +52,8 @@ var ImMessagesSearch = common.Shortcut{
{Name: "no-reactions", Type: "bool", Desc: "skip auto-fetching reactions for each message (default: enrichment enabled)"},
},
Tips: []string{
`Example: lark-cli im +messages-search --query "keyword"`,
`Example: lark-cli im +messages-search --query "keyword" --chat-id <chat_id> --start 2026-07-01 --end 2026-07-08`,
`Example: lark-cli im +messages-search --query "keyword" --as user`,
`Example: lark-cli im +messages-search --query "keyword" --chat-id <chat_id> --start 2026-07-01 --end 2026-07-08 --as user`,
},
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
req, err := buildMessagesSearchRequest(runtime)

View File

@@ -95,6 +95,32 @@ func TestIMTipsExampleFlagsExist(t *testing.T) {
}
}
// TestIMTipsExamplesPinIdentity locks the identity convention on copyable
// examples: user-only shortcuts must pin --as user (a bot-default
// environment would otherwise reject the copied command), and the outbound
// send/reply shortcuts must pin --as bot (governance: never rely on the
// local default identity for deliveries).
func TestIMTipsExamplesPinIdentity(t *testing.T) {
outbound := map[string]bool{"+messages-send": true, "+messages-reply": true}
for _, cmd := range tipsExampleTargets {
sc := shortcutByCommand(t, cmd)
botCapable := false
for _, a := range sc.AuthTypes {
if a == "bot" {
botCapable = true
}
}
for _, example := range exampleCommands(sc) {
if !botCapable && !strings.Contains(example, "--as user") {
t.Errorf("%s: user-only example must pin --as user\nexample: %s", cmd, example)
}
if outbound[cmd] && !strings.Contains(example, "--as bot") {
t.Errorf("%s: outbound example must pin --as bot\nexample: %s", cmd, example)
}
}
}
}
func TestIMTipsFirstExampleCoversRequired(t *testing.T) {
for _, cmd := range tipsExampleTargets {
sc := shortcutByCommand(t, cmd)

View File

@@ -122,11 +122,7 @@ func runFirstExampleDryRun(t *testing.T, command string, wantAPIPath string) {
exampleArgs := firstExampleArgs(t, command)
args := append(exampleArgs, "--dry-run")
req := clie2e.Request{Args: args, WorkDir: t.TempDir()}
if !hasAsFlag(exampleArgs) {
req.DefaultAs = "bot"
}
result, err := clie2e.RunCmd(ctx, req)
result, err := clie2e.RunCmd(ctx, clie2e.Request{Args: args, WorkDir: t.TempDir()})
require.NoError(t, err)
result.AssertExitCode(t, 0)
require.Contains(t, result.Stdout, wantAPIPath,
@@ -159,35 +155,24 @@ var tipsExampleAllTargets = []string{
"+flag-create", "+flag-cancel",
}
// defaultAsForCommand picks the identity to run the dry-run under by reading
// the shortcut's own AuthTypes: "bot" when the shortcut supports bot identity
// (matching the 3 pre-existing path-assertion tests above), otherwise "user"
// for user-only shortcuts (+messages-search and the whole feed/flag series).
func defaultAsForCommand(t *testing.T, command string) string {
t.Helper()
for _, sc := range imshortcuts.Shortcuts() {
if sc.Command != command {
continue
// asFlagValue returns the value following --as in the example, or "".
func asFlagValue(args []string) string {
for i, a := range args {
if a == "--as" && i+1 < len(args) {
return args[i+1]
}
for _, a := range sc.AuthTypes {
if a == "bot" {
return "bot"
}
}
return "user"
}
t.Fatalf("shortcut %s not found", command)
return ""
}
// TestIMTipsAllExamplesDryRun extends the executability lock from the 3
// path-assertion tests above (messages-send, chat-messages-list,
// resources-download) to every "Example:" tip of all 18 shortcuts: each
// example, with placeholders substituted and --dry-run appended, must exit 0.
// This only asserts exit code, not the API path — the 3 tests above keep
// that stronger assertion for their targets. Examples that already carry an
// explicit --as run verbatim; only --as-less examples get an identity
// injected (matching each shortcut's AuthTypes).
// example, with placeholders substituted and --dry-run appended, runs
// VERBATIM — no identity is injected, so the test proves the copied example
// itself is runnable, not a framework-completed variant of it. Examples that
// carry an explicit --as additionally assert the resolved identity equals
// that value.
func TestIMTipsAllExamplesDryRun(t *testing.T) {
for _, cmd := range tipsExampleAllTargets {
for i, exampleArgs := range allExampleArgs(t, cmd) {
@@ -200,16 +185,23 @@ func TestIMTipsAllExamplesDryRun(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req := clie2e.Request{
Args: append(append([]string{}, exampleArgs...), "--dry-run"),
result, err := clie2e.RunCmd(ctx, clie2e.Request{
Args: append(append([]string{}, exampleArgs...), "--dry-run", "--json"),
WorkDir: t.TempDir(),
}
if !hasAsFlag(exampleArgs) {
req.DefaultAs = defaultAsForCommand(t, cmd)
}
result, err := clie2e.RunCmd(ctx, req)
})
require.NoError(t, err)
require.NoError(t, result.RunErr, "binary: %s args: %v", result.BinaryPath, result.Args)
result.AssertExitCode(t, 0)
if wantAs := asFlagValue(exampleArgs); wantAs != "" {
var envelope struct {
Identity string `json:"identity"`
}
require.NoError(t, json.Unmarshal([]byte(result.Stdout), &envelope),
"dry-run --json stdout should be a JSON envelope")
require.Equal(t, wantAs, envelope.Identity,
"example pins --as %s, resolved identity must match", wantAs)
}
})
}
}