refactor(modules): allow hyphens in module names

Relax module name regex to accept hyphens, preparing for hyphenated
loldle variants (loldle-emoji, loldle-quote, etc.) ported from upstream
JS sources. Storage prefix delimiter ':' remains rejected. Telegram
command names use separate stricter regex (commandNameRe) and are
unaffected.
This commit is contained in:
2026-05-09 12:19:16 +07:00
parent 998016f7f9
commit 9e95db2851
2 changed files with 27 additions and 5 deletions
+10 -4
View File
@@ -2,15 +2,21 @@ package modules
import (
"fmt"
"regexp"
"sort"
"github.com/tiennm99/miti99bot-go/internal/storage"
)
// moduleNameRe enforces the same alphabet as command names so KV prefix
// isolation is preserved (no ":" in module names → no prefix collision) and
// the cron route's path segment regex matches.
var moduleNameRe = commandNameRe // alias kept for symmetry; one regex serves both
// moduleNameRe is intentionally looser than commandNameRe — it allows hyphen
// so modules can keep their JS-source names verbatim (e.g. "loldle-emoji").
// The crucial constraint is "no `:`" so the storage Prefixed wrapper's `:`
// delimiter cannot be subverted; everything else is style.
//
// Telegram command names still need the stricter [a-z0-9_]{1,32} alphabet
// (commandNameRe in validate.go). Cron route segments use their own regex in
// internal/server/router.go and stay strict for log-injection safety.
var moduleNameRe = regexp.MustCompile(`^[a-z0-9_-]{1,32}$`)
// Registry holds the resolved set of modules selected by the MODULES env var.
// It is built once at startup; Build fails fast on validation or conflict.
+17 -1
View File
@@ -210,7 +210,10 @@ func TestDispatchScheduled_PassesPrefixedDeps(t *testing.T) {
}
func TestBuild_RejectsInvalidModuleName(t *testing.T) {
for _, name := range []string{"BadName", "with-dash", "a:b", ""} {
// `-` is intentionally allowed (loldle-emoji and friends carry hyphenated
// names from the JS source). `:` must stay rejected — it's the storage
// prefix delimiter and a hyphen-allowing regex must not let it through.
for _, name := range []string{"BadName", "a:b", "", "with space", "with.dot", "with/slash"} {
t.Run(name, func(t *testing.T) {
_, err := Build([]string{name}, map[string]Factory{}, newProvider(), nil)
if err == nil {
@@ -220,6 +223,19 @@ func TestBuild_RejectsInvalidModuleName(t *testing.T) {
}
}
func TestBuild_AcceptsHyphenatedModuleName(t *testing.T) {
factories := map[string]Factory{
"loldle-emoji": factory("loldle-emoji", []Command{noopCmd("emoji_cmd")}, nil),
}
reg, err := Build([]string{"loldle-emoji"}, factories, newProvider(), nil)
if err != nil {
t.Fatalf("hyphenated name should be allowed: %v", err)
}
if len(reg.Modules) != 1 || reg.Modules[0].Name != "loldle-emoji" {
t.Errorf("module not registered correctly: %+v", reg.Modules)
}
}
func TestBuild_RejectsDuplicateModuleInEnv(t *testing.T) {
factories := map[string]Factory{
"alpha": factory("alpha", []Command{noopCmd("a1")}, nil),