mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-09-02 18:20:04 +00:00
fix(commands): remove examples from command discovery
This commit is contained in:
@@ -24,7 +24,7 @@ Telegram command names are user-facing contracts. When adding, renaming, or
|
||||
deleting commands, update all related surfaces:
|
||||
|
||||
- module command registration in `internal/modules/<module>/`
|
||||
- command parameter and example metadata used by Telegram and `/help`
|
||||
- command parameter metadata used by Telegram and `/help`
|
||||
- handler usage text and user-facing error text
|
||||
- tests for registration, handlers, and command menu behavior
|
||||
- README/docs when behavior changes are user-visible
|
||||
@@ -34,10 +34,8 @@ Include units or currencies when meaningful (for example, `<vnd_amount>`), use
|
||||
`[...]` for optional input, append `...` for remaining free text, and use
|
||||
parentheses to document structured input (for example,
|
||||
`<ratio(owned:new)>`). Keep metadata, usage errors, examples, and tests exact.
|
||||
Public commands with parameters must provide an explicit copyable example;
|
||||
commands without parameters must omit it. In both Telegram's native menu and
|
||||
`/help`, append `Eg: <invocation>` inline; `/help` must wrap only the invocation
|
||||
in Telegram HTML `<code>` formatting.
|
||||
Telegram's native menu and `/help` show command syntax plus the summary without
|
||||
example invocations.
|
||||
|
||||
## Stats Compatibility
|
||||
|
||||
|
||||
@@ -22,26 +22,23 @@ Disable modules with the `MODULES` environment variable.
|
||||
## Command discovery
|
||||
|
||||
Public command registrations share their description plus optional `Parameters`
|
||||
and `Example` metadata between Telegram's native `/` menu and the bot's `/help`
|
||||
response. Telegram renders the `/command` separately, and its native description
|
||||
field supports only single-line plain text—no copyable code block. For
|
||||
`/stock_buy`, the bot therefore sends this description:
|
||||
metadata between Telegram's native `/` menu and the bot's `/help` response.
|
||||
Telegram renders the `/command` separately, so `/stock_buy` uses this native
|
||||
description:
|
||||
|
||||
```text
|
||||
<quantity> <ticker>. Buy VN stock at market price. Eg: /stock_buy 100 TCB
|
||||
<quantity> <ticker>. Buy VN stock at market price.
|
||||
```
|
||||
|
||||
`/help` combines the command syntax and summary, then appends `Eg:` and the
|
||||
example on the same line. It wraps only the invocation in Telegram HTML
|
||||
`<code>` formatting so it is easy to copy. Public commands with parameters must
|
||||
register an explicit example; commands without parameters omit it entirely.
|
||||
`/help` combines the full command syntax and summary on one line. Neither
|
||||
discovery surface includes example invocations.
|
||||
|
||||
For future commands, use lowercase descriptive parameter names. Include units
|
||||
or currencies when they affect meaning (`<vnd_amount>`, `<usd_to_spend>`), use
|
||||
square brackets for optional input (`[date]`), append `...` when an argument
|
||||
accepts remaining text (`[target...]`), and describe structured input in
|
||||
parentheses (`<ratio(owned:new)>`, `<options(comma-separated)>`). Keep command
|
||||
metadata, handler usage text, examples, tests, and this documentation aligned.
|
||||
metadata, handler usage text, tests, and this documentation aligned.
|
||||
|
||||
### Stock dividend commands
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) {
|
||||
{
|
||||
Name: "beta",
|
||||
Commands: []modules.Command{
|
||||
{Name: "beta_public", Description: "Beta public", Parameters: "<value>", Example: "/beta_public demo", Visibility: modules.VisibilityPublic},
|
||||
{Name: "beta_public", Description: "Beta public", Parameters: "<value>", Visibility: modules.VisibilityPublic},
|
||||
{Name: "beta_private", Description: "Beta private", Visibility: modules.VisibilityPrivate},
|
||||
},
|
||||
},
|
||||
@@ -38,7 +38,7 @@ func TestBotCommandMenu_UsesLoadedPublicCommandsInModuleOrder(t *testing.T) {
|
||||
|
||||
got := botCommandMenu(reg)
|
||||
want := []models.BotCommand{
|
||||
{Command: "beta_public", Description: "<value>. Beta public. Eg: /beta_public demo"},
|
||||
{Command: "beta_public", Description: "<value>. Beta public."},
|
||||
{Command: "alpha_public", Description: "Alpha public."},
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
@@ -90,15 +90,10 @@ func TestCommandDiscovery_AllPublicCommandsHaveSafeMetadata(t *testing.T) {
|
||||
if got := command.Parameters; got != expectedParameters[command.Name] {
|
||||
t.Errorf("/%s parameters = %q, want %q", command.Name, got, expectedParameters[command.Name])
|
||||
}
|
||||
example := command.ExampleInvocation()
|
||||
if command.Parameters == "" {
|
||||
if example != "" || strings.Contains(command.TelegramMenuDescription(), "Eg:") {
|
||||
t.Errorf("/%s without parameters has example %q", command.Name, example)
|
||||
}
|
||||
} else if !strings.HasPrefix(example, "/"+command.Name) {
|
||||
t.Errorf("/%s example = %q", command.Name, example)
|
||||
}
|
||||
description := command.TelegramMenuDescription()
|
||||
if strings.Contains(description, "Eg:") {
|
||||
t.Errorf("/%s native menu description contains an example: %q", command.Name, description)
|
||||
}
|
||||
if strings.ContainsAny(description, "\r\n") {
|
||||
t.Errorf("/%s native menu description is multiline: %q", command.Name, description)
|
||||
}
|
||||
|
||||
@@ -137,12 +137,10 @@ Successful GIF replies include the result behind Telegram spoiler formatting.
|
||||
The bot registers its Telegram command menu from loaded public modules on
|
||||
every startup. The Go module registry is the single source of truth; no separate
|
||||
command-menu file or manual registration step is required. A command's
|
||||
description plus optional `Parameters` and `Example` metadata feed both
|
||||
surfaces. Telegram renders the command name separately and accepts only a
|
||||
single-line plain-text description, so parameterized commands append
|
||||
`Eg: <invocation>` inline. `/help` uses the same inline layout and wraps only
|
||||
the invocation in Telegram HTML `<code>` formatting. Commands without
|
||||
parameters omit examples.
|
||||
description plus optional `Parameters` metadata feed both surfaces. Telegram
|
||||
renders the command name separately and accepts only a single-line plain-text
|
||||
description. Both the native menu and `/help` show syntax plus the summary and
|
||||
omit example invocations.
|
||||
|
||||
## Operations
|
||||
|
||||
|
||||
@@ -3,58 +3,51 @@
|
||||
## Context
|
||||
|
||||
Telegram's native command menu and `/help` exposed only short descriptions,
|
||||
leaving users to discover parameters and examples through failed invocations or
|
||||
source documentation.
|
||||
leaving users to discover parameters through failed invocations or source
|
||||
documentation.
|
||||
|
||||
## What Changed
|
||||
|
||||
- Extended the shared command registration with `Parameters` and `Example`
|
||||
metadata and presentation helpers used by both discovery surfaces.
|
||||
- Extended the shared command registration with `Parameters` metadata and
|
||||
presentation helpers used by both discovery surfaces.
|
||||
- Added metadata for all 40 public commands, including the exact stats grammar:
|
||||
`[users | user <username> | cmd <command_name>]`.
|
||||
- Normalized placeholders to lowercase descriptive names, including meaningful
|
||||
units or currencies; `[...]` marks optional input, `...` remaining free text,
|
||||
and parentheses structured input.
|
||||
- Normalized `/wheelofnames` to `<options(comma-separated)>` across metadata,
|
||||
usage text, and tests; its example remains
|
||||
`/wheelofnames pizza, sushi, pho`.
|
||||
usage text, and tests.
|
||||
- Finalized dividend placeholders as `<vnd_per_share> <ticker>`,
|
||||
`<ratio(owned:new)> <ticker>`, and
|
||||
`<vnd_per_share> <ratio(owned:new)> <ticker>` for cash, share, and combined
|
||||
commands. Examples and parsing remain unchanged.
|
||||
- Native menu descriptions show only the summary for no-parameter commands.
|
||||
Parameterized commands append an explicit example with the short `Eg:` label
|
||||
on the same plain-text line.
|
||||
- `/help` renders each invocation and summary together. Parameterized commands
|
||||
append `Eg: <code>invocation</code>` on the same line, with only the copyable
|
||||
invocation inside Telegram's HTML `<code>` formatting. No-parameter commands
|
||||
omit the label and example. Dynamic command fields are HTML-escaped.
|
||||
- Registration validation rejects multiline metadata, examples for another
|
||||
command, public commands that provide only one of parameters or example, and
|
||||
public descriptions over Telegram's 256-character limit.
|
||||
commands. Parsing remains unchanged.
|
||||
- Native menu descriptions show parameters followed by the summary, while
|
||||
`/help` renders the complete invocation followed by the summary. Neither
|
||||
discovery surface includes example invocations. Dynamic fields are
|
||||
HTML-escaped in `/help`.
|
||||
- Registration validation rejects multiline metadata and public descriptions
|
||||
over Telegram's 256-character limit.
|
||||
- Updated user and deployment documentation for the shared registry behavior.
|
||||
|
||||
## Reflection
|
||||
|
||||
Keeping syntax and examples beside each handler registration prevents the
|
||||
native menu, `/help`, and implementation from drifting independently. The
|
||||
native surface stays compact, while `/help` uses the richer layout Telegram
|
||||
supports without sacrificing safe HTML rendering.
|
||||
Keeping syntax beside each handler registration prevents the native menu,
|
||||
`/help`, and implementation from drifting independently. Both discovery
|
||||
surfaces stay compact without sacrificing safe HTML rendering in `/help`.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Existing command names, handlers, parsers, and persisted data remain
|
||||
unchanged; normalization is presentation-only.
|
||||
- No command was added, renamed, or deleted, so no stats migration is needed.
|
||||
- Public commands with parameters require an explicit example; commands
|
||||
without parameters omit it.
|
||||
- Discovery surfaces intentionally omit example invocations; handler usage
|
||||
errors may still include focused examples.
|
||||
- The complete `/help` output remains within Telegram's 4,096-character limit.
|
||||
|
||||
## Verification
|
||||
|
||||
- Passed: command presentation, validation, menu, and `/help` tests for all 40
|
||||
public commands, including inline `<code>` rendering, no-parameter omission,
|
||||
and both parameter/example validation branches.
|
||||
public commands.
|
||||
- Passed: `go test ./...`, including real MongoDB Testcontainers suites.
|
||||
- Passed: `go vet ./...`
|
||||
- Passed: `go build ./...`
|
||||
@@ -62,5 +55,5 @@ supports without sacrificing safe HTML rendering.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Require parameter and example metadata updates alongside future public
|
||||
command contract changes.
|
||||
- Require parameter metadata updates alongside future public command contract
|
||||
changes.
|
||||
|
||||
@@ -16,7 +16,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Show current crypto price in USD",
|
||||
Parameters: "<coin>",
|
||||
Example: "/coin_price BTC",
|
||||
Handler: s.handlePrice,
|
||||
},
|
||||
{
|
||||
@@ -24,7 +23,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Top up USD to your coin account",
|
||||
Parameters: "<usd_amount>",
|
||||
Example: "/coin_topup 1000",
|
||||
Handler: s.handleTopup,
|
||||
},
|
||||
{
|
||||
@@ -32,7 +30,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Spend a USD amount to buy coin",
|
||||
Parameters: "<coin> <usd_to_spend>",
|
||||
Example: "/coin_buy BTC 10",
|
||||
Handler: s.handleBuy,
|
||||
},
|
||||
{
|
||||
@@ -40,7 +37,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Sell enough coin to receive a USD amount",
|
||||
Parameters: "<coin> <usd_to_receive>",
|
||||
Example: "/coin_sell BTC 10",
|
||||
Handler: s.handleSell,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,11 +17,6 @@ func (c Command) InvocationSentence() string {
|
||||
return withTerminalPunctuation(c.Invocation())
|
||||
}
|
||||
|
||||
// ExampleInvocation returns the explicitly registered copyable example.
|
||||
func (c Command) ExampleInvocation() string {
|
||||
return strings.TrimSpace(c.Example)
|
||||
}
|
||||
|
||||
// SummarySentence normalizes a command summary to a sentence without
|
||||
// duplicating terminal punctuation supplied by the registration.
|
||||
func (c Command) SummarySentence() string {
|
||||
@@ -38,10 +33,6 @@ func (c Command) TelegramMenuDescription() string {
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
sb.WriteString(c.SummarySentence())
|
||||
if example := c.ExampleInvocation(); example != "" {
|
||||
sb.WriteString(" Eg: ")
|
||||
sb.WriteString(example)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,26 +7,22 @@ func TestCommandPresentation(t *testing.T) {
|
||||
name string
|
||||
command Command
|
||||
invocation string
|
||||
example string
|
||||
menu string
|
||||
}{
|
||||
{
|
||||
name: "parameters and explicit example",
|
||||
name: "parameters",
|
||||
command: Command{
|
||||
Name: "stock_buy",
|
||||
Parameters: "<quantity> <ticker>",
|
||||
Description: "Buy VN stock at market price",
|
||||
Example: "/stock_buy 100 TCB",
|
||||
},
|
||||
invocation: "/stock_buy <quantity> <ticker>",
|
||||
example: "/stock_buy 100 TCB",
|
||||
menu: "<quantity> <ticker>. Buy VN stock at market price. Eg: /stock_buy 100 TCB",
|
||||
menu: "<quantity> <ticker>. Buy VN stock at market price.",
|
||||
},
|
||||
{
|
||||
name: "no parameters omits example",
|
||||
name: "no parameters",
|
||||
command: Command{Name: "ping", Description: "Health check!"},
|
||||
invocation: "/ping",
|
||||
example: "",
|
||||
menu: "Health check!",
|
||||
},
|
||||
{
|
||||
@@ -35,11 +31,9 @@ func TestCommandPresentation(t *testing.T) {
|
||||
Name: "random",
|
||||
Parameters: "<options(comma-separated)>",
|
||||
Description: "Pick one option",
|
||||
Example: "/random pizza, sushi",
|
||||
},
|
||||
invocation: "/random <options(comma-separated)>",
|
||||
example: "/random pizza, sushi",
|
||||
menu: "<options(comma-separated)>. Pick one option. Eg: /random pizza, sushi",
|
||||
menu: "<options(comma-separated)>. Pick one option.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -48,9 +42,6 @@ func TestCommandPresentation(t *testing.T) {
|
||||
if got := tc.command.Invocation(); got != tc.invocation {
|
||||
t.Errorf("Invocation() = %q, want %q", got, tc.invocation)
|
||||
}
|
||||
if got := tc.command.ExampleInvocation(); got != tc.example {
|
||||
t.Errorf("ExampleInvocation() = %q, want %q", got, tc.example)
|
||||
}
|
||||
if got := tc.command.TelegramMenuDescription(); got != tc.menu {
|
||||
t.Errorf("TelegramMenuDescription() = %q, want %q", got, tc.menu)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Top up VND to your gold account",
|
||||
Parameters: "<vnd_amount>",
|
||||
Example: "/gold_topup 5000000",
|
||||
Handler: s.handleTopup,
|
||||
},
|
||||
{
|
||||
@@ -29,7 +28,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Buy gold at SJC sell price",
|
||||
Parameters: "<luong>",
|
||||
Example: "/gold_buy 1",
|
||||
Handler: s.handleBuy,
|
||||
},
|
||||
{
|
||||
@@ -37,7 +35,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Sell gold at SJC buy price",
|
||||
Parameters: "<luong>",
|
||||
Example: "/gold_sell 0.5",
|
||||
Handler: s.handleSell,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "LoL matches for a date (dd, dd-mm, dd/mm, ddmm, or full date; default today)",
|
||||
Parameters: "[date]",
|
||||
Example: "/lol 21-07-2026",
|
||||
Handler: s.handleSchedule,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,7 +21,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Classic loldle — guess the current champion",
|
||||
Parameters: "[champion]",
|
||||
Example: "/loldle Ahri",
|
||||
Handler: s.handleLoldle,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -129,17 +129,14 @@ func senderMention(u *models.User) string {
|
||||
|
||||
func disclaimerCommand(name, description, defaultTarget string, allowCustomTarget bool) modules.Command {
|
||||
parameters := ""
|
||||
example := ""
|
||||
if allowCustomTarget {
|
||||
parameters = "[target...]"
|
||||
example = "/" + name + " FBI"
|
||||
}
|
||||
return modules.Command{
|
||||
Name: name,
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: description,
|
||||
Parameters: parameters,
|
||||
Example: example,
|
||||
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
|
||||
@@ -31,7 +31,6 @@ func randomCommand() modules.Command {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Pick one random comma-separated option",
|
||||
Parameters: "<options(comma-separated)>",
|
||||
Example: "/random pizza, sushi, pho",
|
||||
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
|
||||
@@ -25,7 +25,6 @@ func wheelOfNamesCommand() modules.Command {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Pick one comma-separated option with wheel GIF when configured",
|
||||
Parameters: "<options(comma-separated)>",
|
||||
Example: "/wheelofnames pizza, sushi, pho",
|
||||
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
|
||||
@@ -40,7 +40,6 @@ type Command struct {
|
||||
Visibility Visibility // public/protected/private
|
||||
Description string // concise summary shown in command discovery (required, non-empty)
|
||||
Parameters string // optional syntax after the command, e.g. "<quantity> <ticker>"
|
||||
Example string // full invocation required with public Parameters; omitted without them
|
||||
Handler CommandHandler // required
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ func statsCommand(c *counter) modules.Command {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Show command usage statistics",
|
||||
Parameters: "[users | user <username> | cmd <command_name>]",
|
||||
Example: "/stats user alice",
|
||||
Handler: func(ctx context.Context, b *bot.Bot, update *models.Update) error {
|
||||
if update.Message == nil {
|
||||
return nil
|
||||
|
||||
@@ -15,7 +15,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Show current VN stock price",
|
||||
Parameters: "<ticker>",
|
||||
Example: "/stock_price TCB",
|
||||
Handler: s.handlePrice,
|
||||
},
|
||||
{
|
||||
@@ -23,7 +22,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Top up VND to your stock account",
|
||||
Parameters: "<vnd_amount>",
|
||||
Example: "/stock_topup 5000000",
|
||||
Handler: s.handleTopup,
|
||||
},
|
||||
{
|
||||
@@ -31,7 +29,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Buy VN stock at market price",
|
||||
Parameters: "<quantity> <ticker>",
|
||||
Example: "/stock_buy 100 TCB",
|
||||
Handler: s.handleBuy,
|
||||
},
|
||||
{
|
||||
@@ -39,7 +36,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Sell VN stock back to VND",
|
||||
Parameters: "<quantity> <ticker>",
|
||||
Example: "/stock_sell 100 TCB",
|
||||
Handler: s.handleSell,
|
||||
},
|
||||
{
|
||||
@@ -47,7 +43,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Record cash dividend",
|
||||
Parameters: "<vnd_per_share> <ticker>",
|
||||
Example: "/stock_cash_dividend 1500 TCB",
|
||||
Handler: s.handleCashDividend,
|
||||
},
|
||||
{
|
||||
@@ -55,7 +50,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Record share dividend",
|
||||
Parameters: "<ratio(owned:new)> <ticker>",
|
||||
Example: "/stock_share_dividend 100:10 TCB",
|
||||
Handler: s.handleShareDividend,
|
||||
},
|
||||
{
|
||||
@@ -63,7 +57,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Record cash and share dividend",
|
||||
Parameters: "<vnd_per_share> <ratio(owned:new)> <ticker>",
|
||||
Example: "/stock_dividend 1500 100:10 TCB",
|
||||
Handler: s.handleDividend,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,9 +50,6 @@ func RenderHelp(reg *modules.Registry) string {
|
||||
fmt.Fprintf(&sb, "\n%s %s",
|
||||
html.EscapeString(command.InvocationSentence()),
|
||||
html.EscapeString(command.SummarySentence()))
|
||||
if example := command.ExampleInvocation(); example != "" {
|
||||
fmt.Fprintf(&sb, " Eg: <code>%s</code>", html.EscapeString(example))
|
||||
}
|
||||
}
|
||||
sections = append(sections, sb.String())
|
||||
}
|
||||
|
||||
@@ -74,13 +74,12 @@ func TestRenderHelp_GroupsByModuleAndSkipsNonPublic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHelp_ShowsParametersAndCopyableExample(t *testing.T) {
|
||||
func TestRenderHelp_ShowsParametersWithoutExample(t *testing.T) {
|
||||
command := modules.Command{
|
||||
Name: "buy",
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Buy & hold",
|
||||
Parameters: "<quantity> <ticker>",
|
||||
Example: "/buy 100 TCB & hold",
|
||||
Handler: helpTestNoop,
|
||||
}
|
||||
reg, err := modules.Build(
|
||||
@@ -97,14 +96,8 @@ func TestRenderHelp_ShowsParametersAndCopyableExample(t *testing.T) {
|
||||
if !strings.Contains(out, "/buy <quantity> <ticker>. Buy & hold.") {
|
||||
t.Fatalf("help missing formatted invocation and summary:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "<code>/buy 100 TCB & hold</code>") {
|
||||
t.Fatalf("help missing copyable example:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "Buy & hold. Eg: <code>/buy 100 TCB & hold</code>") {
|
||||
t.Fatalf("help missing short example label:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "<pre>") {
|
||||
t.Fatalf("help example should use inline code, not a pre block:\n%s", out)
|
||||
if strings.Contains(out, "Eg:") || strings.Contains(out, "<code>") || strings.Contains(out, "<pre>") {
|
||||
t.Fatalf("help should not render examples:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,25 +29,6 @@ func validateCommand(c Command) error {
|
||||
if strings.ContainsAny(c.Parameters, "\r\n") {
|
||||
return fmt.Errorf("command %q: parameters must be single-line", c.Name)
|
||||
}
|
||||
if strings.ContainsAny(c.Example, "\r\n") {
|
||||
return fmt.Errorf("command %q: example must be single-line", c.Name)
|
||||
}
|
||||
parameters := strings.TrimSpace(c.Parameters)
|
||||
example := strings.TrimSpace(c.Example)
|
||||
if c.Visibility == VisibilityPublic {
|
||||
if parameters == "" && example != "" {
|
||||
return fmt.Errorf("command %q: example requires parameters", c.Name)
|
||||
}
|
||||
if parameters != "" && example == "" {
|
||||
return fmt.Errorf("command %q: example is required when parameters are present", c.Name)
|
||||
}
|
||||
}
|
||||
if example != "" {
|
||||
prefix := "/" + c.Name
|
||||
if example != prefix && !strings.HasPrefix(example, prefix+" ") {
|
||||
return fmt.Errorf("command %q: example must invoke %s", c.Name, prefix)
|
||||
}
|
||||
}
|
||||
if c.Visibility == VisibilityPublic && utf8.RuneCountInString(c.TelegramMenuDescription()) > telegramCommandDescriptionMaxRunes {
|
||||
return fmt.Errorf("command %q: Telegram menu description exceeds %d characters", c.Name, telegramCommandDescriptionMaxRunes)
|
||||
}
|
||||
|
||||
@@ -47,22 +47,6 @@ func TestValidateCommand_RejectsInvalidPresentationMetadata(t *testing.T) {
|
||||
name: "multiline parameters",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: "d", Parameters: "<a>\n<b>", Handler: okHandler},
|
||||
},
|
||||
{
|
||||
name: "multiline example",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: "d", Example: "/ok one\n/ok two", Handler: okHandler},
|
||||
},
|
||||
{
|
||||
name: "example invokes another command",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: "d", Example: "/other", Handler: okHandler},
|
||||
},
|
||||
{
|
||||
name: "example without parameters",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: "d", Example: "/ok", Handler: okHandler},
|
||||
},
|
||||
{
|
||||
name: "parameters without example",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: "d", Parameters: "<value>", Handler: okHandler},
|
||||
},
|
||||
{
|
||||
name: "native description too long",
|
||||
command: Command{Name: "ok", Visibility: VisibilityPublic, Description: strings.Repeat("x", 257), Handler: okHandler},
|
||||
@@ -86,13 +70,12 @@ func TestValidateCommand_AcceptsLegalNames(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCommand_AcceptsMatchedParametersAndExample(t *testing.T) {
|
||||
func TestValidateCommand_AcceptsParameters(t *testing.T) {
|
||||
command := Command{
|
||||
Name: "ok",
|
||||
Visibility: VisibilityPublic,
|
||||
Description: "Do it",
|
||||
Parameters: "<value>",
|
||||
Example: "/ok demo",
|
||||
Handler: okHandler,
|
||||
}
|
||||
if err := validateCommand(command); err != nil {
|
||||
|
||||
@@ -23,7 +23,6 @@ func New(deps modules.Deps) modules.Module {
|
||||
Visibility: modules.VisibilityPublic,
|
||||
Description: "Classic wordle — guess the 5-letter word",
|
||||
Parameters: "[word]",
|
||||
Example: "/wordle apple",
|
||||
Handler: s.handleWordle,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user