Skip to content

feat(bot): add a command module registry so new commands live in thei… - #732

Open
danvenger wants to merge 48 commits into
mimir-stellar:mainfrom
danvenger:feat/command-registry-327
Open

danvenger wants to merge 48 commits into
mimir-stellar:mainfrom
danvenger:feat/command-registry-327

Conversation

@danvenger

Copy link
Copy Markdown
Contributor

Description:
• Commands are now modules (src/commands/types.ts) listed in order in src/commands/index.ts. /contracts and /preview are the first examples.
• registerCommandHandlers and registerCommands walk the registry. Operator-only and allowlist checks run in one place, based on each module's flags.
• /help and setMyCommands follow registry order. Every reply is exactly the same as before.
• Cleaned up leftover merge damage in src/bot.ts and src/notifications/format.ts that kept main from loading.
• Updated CONTRIBUTING, README and CHANGELOG.

Testing:
• New tests/command-registry.test.mjs: 22 passed
• tests/bot.test.mjs (11) and tests/help-command.test.mjs (5) pass unchanged
Closes #327

…r own files

Slash commands are now `CommandModule`s (src/commands/types.ts: command,
description, operatorOnly, allowlisted, hidden, handle(ctx, deps)) listed in
menu order by `COMMAND_MODULES` in src/commands/index.ts.

- registerCommandHandlers walks the registry and applies the operator-only
  and ALLOWED_CHAT_IDS gates centrally from module flags (same silent ignore
  and log lines as before), after the per-chat rate limiter.
- registerCommands / setMyCommands and /help list module commands in
  registry order; COMMANDS is derived from the registry.
- /contracts and /preview are written in the full module shape as the
  first examples; replies are byte-identical.
- createCommandModule normalises a definition (flag defaults, description
  from COMMAND_DESCRIPTIONS); defineCommand still registers ad-hoc modules
  and accepts the older handler/chatAllowlist spellings.
- src/bot.ts is rebuilt around the registry, dropping the duplicated
  pre-registry handler table and helpers left by earlier merges that kept
  main from compiling; the unknown-command hint and rate limiter are wired
  through the same path.
- src/notifications/format.ts: resolve the duplicated claim_created cases
  and `head` declarations left by a merge (load-time SyntaxError).
- tests/command-registry.test.mjs covers names, order and authorization;
  CONTRIBUTING/README/CHANGELOG describe the module pattern.

Closes mimir-stellar#327
The scan-secrets CI gate flagged 11 fixture strings in five test files
(address-activity, audit-check-cli, cycle-summary, health-auth,
safe-error-message). They are fake values, so build them with
[...].join("") like the rest of the suite does instead of weakening
the scanner. Values are unchanged.
…merge

68c5530 (mimir-stellar#546, health bearer token) replaced instead of extended the
tail of BotConfig, the collector's network() parser and the loadConfig
fields. That removed linkPreviewMarket/Squad, routes, logSample*,
logBufferLines, telegramWebhookUrl, telegramApiRoot and mimirAppUrl and
broke loadConfig at runtime (c.network is not a function). Restore them
from the parent commit, keeping healthAuthToken.
…ier moved

mimir-stellar#698 added SendExtra.eventName and disable_notification for SILENT_EVENTS
in bot.ts; mimir-stellar#709 moved createNotifier to src/notifications/notifier.ts
from an older copy, so the poller's eventName no longer typechecked and
low-priority events were sent loudly again. Re-apply the mimir-stellar#698 change.
- commands/unknown.ts (mimir-stellar#726): non-null reads in the bounded levenshtein rows
- notifications/amount-display.ts (mimir-stellar#728): USCD_DECIMALS typo; accept the
  documented string input by converting to bigint before formatUsdc
- rpc-compare-cli.ts (mimir-stellar#701): drop unused imports/params, guard indexed push
- i18n.ts: drop the orphan 'ping' description. /ping is not in
  COMMAND_NAMES (not registered since mimir-stellar#700) and has no es translation,
  which also broke the en/es description parity test.
…neric-decode

generic-decode (mimir-stellar#543) was written against stellar-sdk 16 (switch(),
value()) but merged after the bump to 17 (mimir-stellar#527), where an ScVal exposes
its arm as a 'type' string and its payload as a 'value' getter. Accept
both shapes, and update the test helpers to the SDK 17 constructors
(Uint128Parts/Uint256Parts, no TimePoint/Duration wrappers).
…pe validation

mimir-stellar#658 started rejecting BOT_TOKEN values that are not <digits>:<35+ chars>,
which also rejected the mock profile's deliberate MOCK-PROFILE-NOT-A-BOT-TOKEN
placeholder, so MIMIR_PROFILE=mock no longer loaded without a real token.
… shape

mimir-stellar#658 made loadConfig reject tokens whose secret part is shorter than 35
url-safe characters, but the config-loading fixtures still used short
fakes (123:fake, 34 As, ...). Lengthen them; they stay obviously fake and
are still built from parts so the secret scan stays clean.
17 test files imported ../src/*.js or ../src/*.ts directly. CI runs plain
'node --test' against the build, where ../src/*.js does not exist and
.ts files cannot resolve their own .js imports, so these files failed
before running a single test. Point them at ../dist/ (186 other test
files already do).
mimir-stellar#546 reads HEALTH_AUTH_TOKEN and documents it in .env.example, but never
added it to CONFIG_KEYS, so the .env.example consistency check (mimir-stellar#723) and
config provenance did not know about it. Mark it secret.
/ping has not been registered since mimir-stellar#700 (the help and menu tests pin the
11 registered handlers), so the generated command reference no longer
lists it. Regenerated with scripts/gen-command-docs.mjs.
…llar-sdk 17

stellar-sdk 17 (mimir-stellar#527) makes scValToNative return undefined for anything
that is not an ScVal where 16 threw, so a garbage event value decoded as
an empty payload instead of an 'unknown' one. Reject non-ScVal input in
decode's native() so malformed values keep surfacing as unknown.

Port the affected tests' ScVal builders to the SDK 17 API: scvU64 takes a
bigint (no 'new xdr.Uint64'), and ScVal maps convert with scValToNative
instead of .map()/entry.key()/entry.val().
scripts/validate-fixtures.mjs (mimir-stellar#652) expected a flat array of
{id, contract, ledger, name} events, but tests/fixtures/events.json is the
documented {config, cases: [{id, kind, expect, event}]} catalog, so it
rejected the real fixtures ('fixtures must be an array of events'). Validate
the catalog as docs/contributor-fixtures.md describes, and pick up an
events.json from the working directory as tests/fixtures.test.mjs expects.
Cases are independent, so the per-contract monotonic ledger rule is dropped.
81f727f added a category icon to claim_created notifications but did not
refresh tests/fixtures/golden, so the determinism check failed. Regenerated
with 'node scripts/gen-golden-corpus.mjs --update'; only the five
claim_created fixtures change.
 and mimir-stellar#609

- i128-amounts: mimir-stellar#639 renders every settled-market share with its percentage
  of the gross; the i128-max case still expected the old single line.
- ordering: mimir-stellar#609 rejects malformed getEvents pages and non-object entries
  with RpcResponseShapeError (so the poller keeps its cursor), but the
  older ordering tests expected them to be skipped as empty pages. Assert
  the rejection and keep the decode-to-unknown coverage for entries that
  pass the guard.
…, mimir-stellar#672, mimir-stellar#674) were never merged

Each of these PRs landed only its test file; the module under test never
reached main, so the files failed at import. Skip them with an explicit
reason pointing at the PR instead of deleting them.
…mounts

escapeMarkdownV2 listed '+' twice and missed '-', so a negative amount
would be rejected by Telegram's entity parser. displayUsdc('') rendered
'0' because BigInt('') is 0n; an empty amount now throws.
Several inputs were off by a factor of ten from the displayed values, a
few relied on 3 decimals despite the default maxDecimals of 2, and the
2^63 case and the reserved-character string were miscomputed.
mimir-stellar#647 pinned a '+5s' / '+2 30' format that chainClockLabel never had and
referenced an undefined NOY. The wording is user-visible and has been
'local clock 5.0s ahead of chain' since mimir-stellar#288, so the test was wrong.
mimir-stellar#645 was written for an EVM-shaped API (0x hashes, chainId, a single
event argument) and referenced an undefined markerEvent. Exercise the
real eventExplorerUrl(config, event) / explorerKeyboard(config, event)
with Stellar events and 64-hex hashes, covering the same cases.
The mimir-stellar#660 test expected unescaped text and 'Test Network'/'Public Network'
labels that aboutMessage never produced; the message text is user-visible
and stays as is.
mimir-stellar#668 tested a 'Notifications sent: N' layout the builder never emitted
and called the nonexistent assert.typeof. Pin the real output and cover
multi-day uptime.
mimir-stellar#665 guessed at record()/stats() shapes, percentiles and a sample window
that RpcTiming never had; by design (mimir-stellar#309) it keeps O(1) aggregates only.
Assert the real summary() figures and the exact formatRpcTiming line.
With no last run on record, start() fired immediately (stamped with the
upcoming slot) and then fired again when that slot arrived. A first start
now waits for the next boundary; missed-run catch-up is unchanged.
advanceTo yielded two microtasks, too few for FileLastRunStore's real
reads, so the scheduled tick never finished before the assertions. Wait
until the scheduler re-arms. mkdtemp used a relative prefix and created
its directories inside the repo.
The logger has always normalised to lowercase and supports only text and
json; mimir-stellar#673 expected uppercase values and a 'pretty' format.
…ref id

The per-chat cap alias was mangled to 'pathMaxFollowsPerChat' in the mimir-stellar#676
merge, so it was silently ignored. A ref like 'market:a:b' was accepted
with id 'a:b'; ids may no longer contain ':'.
The suite runs under node --test and Jest is not a dependency, so the
file failed at import of @jest/globals.
…imir-stellar#709 suites

The mimir-stellar#700 merge interleaved two versions of this file into a module that
declared deps twice and could not load. Restore every test from both,
using mimir-stellar#700's names (validatePauseConfirmData, pauseConfirmKeyboard,
handlePauseConfirmCallback). mimir-stellar#709's '/pause calls pause() at once' case
is dropped: /pause sends mimir-stellar#694's confirmation prompt.
…only

withReadOnlyDir chmod'ed the temp dir to 0555 before each case wrote its
'previous' file, so every case died with EACCES in its own setup and never
reached the writer under test. Make the dir read-only after seeding (the
cursor case already did), and skip with a reason when running as root.
…es in the temp dir

Only the cursor, counters, lock and status files were redirected, so each
smoke run (in CI and from e2e-smoke-script.test.mjs) wrote
data/send-journal.jsonl into the repo and broke hermetic-data's
data/-holds-only-.gitkeep check.
…lar#609

mimir-stellar#609's rpc-guard rejects getEvents pages whose events lack a string id or
are not objects (RpcResponseShapeError), so the poller keeps its cursor.
The older mimir-stellar#232/mimir-stellar#310 cases still expected those events to be scanned and
deduplicated by composite key. Assert the rejection, keep the malformed-
XDR degrade case with a valid id, and leave composite-key coverage to
dedup.test.mjs.
makeConfig only redirected cursorFile, so pollers started by these tests
wrote counters, status, lock, audit and send-journal files into the repo's
data/ through loadConfig's defaults.
It is a placeholder, never a real transaction, so it must not become an
explorer link. Pinned by tx-hash.test.mjs (mimir-stellar#651).
…red)

A config object without the field (anything built before mimir-stellar#546, or by
hand) made every health route answer 401, because only null meant 'no
token'. Non-loopback binds still require a token at config load.
'run' was passed to node as the script path, so node tried to load
./run and both CLI cases failed with MODULE_NOT_FOUND / exit 1.
…ir-stellar#654

mimir-stellar#654 (a test-only issue) also rewrote src/redact.ts: URL userinfo was
replaced with the literal '{$REDACTED}C', query credentials with '$d{$REDACTED}',
the userinfo pattern no longer required 'user:pass', and the bot-in-URL
token rule consumed a trailing character instead of looking ahead.
Restore the file to its pre-mimir-stellar#654 state (only comment text differs
otherwise).
redactChatId has always kept the sign and last four digits (or an
@username's first character), and /filters shows that to operators; mimir-stellar#654
expected '[REDACTED]' for every input.
…o's data/

The clean-checkout end-to-end test loads the mock profile and overrode
only some state paths, so the poller fell back to ./data/counters.json
and ./data/send-journal.jsonl and left them behind after npm test.
Point every state file at the test's temp dir, as scripts/e2e-smoke.mjs
already does.
danvenger pushed a commit to danvenger/telegram-bot that referenced this pull request Oct 23, 2026
…unks as mimir-stellar#732)

Upstream main does not pass its own CI, so this PR's checks cannot go green
on their own. Bring in the main-repair hunks from mimir-stellar#732 (feat/command-registry-327),
line-for-line identical so the two PRs merge cleanly, without the
command-registry feature itself (src/commands/types.ts, index/registry
rewrite, command-registry tests and docs are NOT included):

- tests: build token-shaped fixtures from parts so `npm run scan:secrets`
  passes (address-activity, audit-check-cli, cycle-summary, health-auth,
  safe-error-message)
- src/bot.ts: drop the duplicated pre-registry handler table and helpers
  left by earlier merges (39 syntax errors); taken from mimir-stellar#732 except that
  dispatch() keeps main's CommandModule field names (operatorOnly/
  chatAllowlist/handler); the rate-limiter deps mimir-stellar#732 adds to CommandDeps
  are declared via a module augmentation in src/commands/rate-limit.ts
- config/notifier/i18n/amount-display/rpc-compare/generic-decode and the
  rest of mimir-stellar#732's main-repair fixes and test realignments that typecheck,
  build, `npm test` and the e2e smoke need
enliven17 pushed a commit that referenced this pull request Oct 25, 2026
…rders (#734)

* feat(bot): render squad resolution with pool totals and payout ratio

* fix(ci): repair merge damage and secret-scan findings on main (same hunks as #732)

Upstream main does not pass its own CI, so this PR's checks cannot go green
on their own. Bring in the main-repair hunks from #732 (feat/command-registry-327),
line-for-line identical so the two PRs merge cleanly, without the
command-registry feature itself (src/commands/types.ts, index/registry
rewrite, command-registry tests and docs are NOT included):

- tests: build token-shaped fixtures from parts so `npm run scan:secrets`
  passes (address-activity, audit-check-cli, cycle-summary, health-auth,
  safe-error-message)
- src/bot.ts: drop the duplicated pre-registry handler table and helpers
  left by earlier merges (39 syntax errors); taken from #732 except that
  dispatch() keeps main's CommandModule field names (operatorOnly/
  chatAllowlist/handler); the rate-limiter deps #732 adds to CommandDeps
  are declared via a module augmentation in src/commands/rate-limit.ts
- config/notifier/i18n/amount-display/rpc-compare/generic-decode and the
  rest of #732's main-repair fixes and test realignments that typecheck,
  build, `npm test` and the e2e smoke need

---------

Co-authored-by: danvenger <342099042+danvenger@users.noreply.github.com>
Co-authored-by: v <v@v>
…fail the run

poller.stop() does not wait for an in-flight cycle, so on Node 20 the
cursor writer could still be creating files when rm() ran, failing with
ENOTEMPTY. Retry the removal briefly and ignore an already-gone dir.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(bot): add a command module registry so new commands live in their own files

1 participant