mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-04-17 19:22:09 +00:00
grammY-based bot with a module plugin system loaded from the MODULES env var. Three command visibility levels (public/protected/private) share a unified command namespace with conflict detection at registry build. - 4 initial modules (util, wordle, loldle, misc); util fully implemented, others are stubs proving the plugin system end-to-end - util: /info (chat/thread/sender ids) + /help (pure renderer over the registry, HTML parse mode, escapes user-influenced strings) - KVStore interface with CFKVStore and a per-module prefixing factory; getJSON/putJSON convenience helpers; other backends drop in via one file - Webhook at POST /webhook with secret-token validation via grammY's webhookCallback; no admin HTTP surface - Post-deploy register script (npm run deploy = wrangler deploy && node --env-file=.env.deploy scripts/register.js) for setWebhook and setMyCommands; --dry-run flag for preview - 56 vitest unit tests across 7 suites covering registry, db wrapper, dispatcher, help renderer, validators, and HTML escaper - biome for lint + format; phased implementation plan under plans/
8.1 KiB
8.1 KiB
Phase 08 — Tests
Context Links
- Plan: plan.md
- Phases 03, 04, 05
Overview
- Priority: P1
- Status: pending
- Description: vitest unit tests for pure-logic modules — registry, DB prefixing wrapper (incl.
getJSON/putJSON), dispatcher routing, help renderer, command validators, HTML escaper. NO tests that spin up workerd or hit Telegram.
Key Insights
- Pure-logic testing > integration testing at this stage. Cloudflare's
@cloudflare/vitest-pool-workersadds complexity and slow starts; skip for v1. - Mock
env.KVwith an in-memoryMap-backed fake that implementsget/put/delete/listper the real shape (includinglist_completeandkeys: [{name}]). - For dispatcher tests, build a fake
botthat recordscommand()+on()registrations; assert the right handlers were wired. - For help renderer tests, construct a synthetic registry directly — no need to load real modules.
Requirements
Functional
- Tests run via
npm test(vitest, node env). - No network. No
fetch. No Telegram. - Each test file covers ONE source file.
- Coverage target: registry, db wrapper, dispatcher, help renderer, validators, escaper — these are the logic seams.
Non-functional
- Test suite runs in < 5s.
- No shared mutable state between tests; each test builds its fixtures.
Architecture
tests/
├── fakes/
│ ├── fake-kv-namespace.js # Map-backed KVNamespace impl
│ ├── fake-bot.js # records command() calls
│ └── fake-modules.js # fixture modules for registry tests
├── db/
│ ├── cf-kv-store.test.js
│ └── create-store.test.js
├── modules/
│ ├── registry.test.js
│ ├── dispatcher.test.js
│ └── validate-command.test.js
├── util/
│ └── escape-html.test.js
└── modules/util/
└── help-command.test.js
Related Code Files
Create
tests/fakes/fake-kv-namespace.jstests/fakes/fake-bot.jstests/fakes/fake-modules.jstests/db/cf-kv-store.test.jstests/db/create-store.test.jstests/modules/registry.test.jstests/modules/dispatcher.test.jstests/modules/validate-command.test.jstests/util/escape-html.test.jstests/modules/util/help-command.test.js
Modify
vitest.config.js— confirmenvironment: "node",include: ["tests/**/*.test.js"].
Delete
- none
Test cases (per file)
fake-kv-namespace.js
- In-memory
Map.get(key, {type: "text"})returns value or null.put(key, value, opts?)stores; recordsoptsfor assertions.deleteremoves.list({prefix, limit, cursor})filters by prefix, paginates, returns{keys:[{name}], list_complete, cursor}.
cf-kv-store.test.js
get/put/deleteround-trip with fake KV.list()strips to normalized shape{keys: string[], cursor?, done}.putwithexpirationTtlpasses through to underlying binding.putJSONserializes then callsput; recoverable viagetJSON.getJSONon missing key returnsnull.getJSONon corrupt JSON (manually insert"{not json") returnsnull, does NOT throw, emits a warning.putJSON(key, undefined)throws.
create-store.test.js
createStore("wordle", env).put("k","v")results in raw KV key"wordle:k".createStore("wordle", env).list({prefix:"games:"})calls underlyinglistwith"wordle:games:"prefix.- Returned keys have the
"wordle:"prefix STRIPPED. - Two stores for different modules cannot read each other's keys.
getJSON/putJSONthrough a prefixed store also land at<module>:<key>raw key.- Invalid module name (contains
:) throws.
validate-command.test.js
- Valid command passes for each visibility (public / protected / private).
- Command with leading
/rejected (any visibility). - Command name > 32 chars rejected.
- Command name with uppercase rejected (
COMMAND_NAME_RE=/^[a-z0-9_]{1,32}$/). - Missing description rejected (all visibilities — private also requires description for internal debugging).
- Description > 256 chars rejected.
- Invalid visibility rejected.
registry.test.js
- Fixture: fake modules passed via an injected
moduleRegistrymap (avoidvi.mockpath-resolution flakiness on Windows — phase-04 exposes a loader injection point). MODULES="a,b"loads both;buildRegistryflattens commands into 3 visibility maps + 1allCommandsmap.- Unified namespace conflict: module A registers
/fooas public, module B registers/fooas private →buildRegistrythrows with a message naming both modules AND the command. - Same-visibility conflict (two modules, both public
/foo) → throws. - Unknown module name → throws with message.
- Empty
MODULES→ throws. initcalled once per module with{db, env}; db is a namespaced store (assert key prefix by doing aputthrough the injected db and checking raw KV).- After
buildRegistry,getCurrentRegistry()returns the same instance;resetRegistry()clears it and subsequentgetCurrentRegistry()throws.
dispatcher.test.js
- Build registry from fake modules (one each: public, protected, private), install on fake bot.
- Assert
bot.command()called for EVERY entry — including private ones (the whole point: unified routing). - Assert no other bot wiring (no
bot.on, nobot.hears). Dispatcher is minimal. - Call count = total commands across all visibilities.
help-command.test.js
- Build a synthetic registry with three modules: A (1 public), B (1 public + 1 protected), C (1 private only).
- Invoke help handler with a fake
ctxthat capturesreply(text, opts). - Assert output:
- Contains
<b>A</b>and<b>B</b>. - Does NOT contain
<b>C</b>(no visible commands — private is hidden from help). - Does NOT contain C's private command name anywhere in output.
- Protected command has
(protected)suffix. opts.parse_mode === "HTML".- HTML-escapes a module description containing
<script>.
- Contains
escape-html.test.js
- Escapes
&,<,>,". - Leaves safe chars alone.
Implementation Steps
- Build fakes first —
fake-kv-namespace.js,fake-bot.js,fake-modules.js. - Write tests file-by-file, running
npm testafter each. - If a test reveals a bug in the source, fix the source (not the test).
- Final full run, assert all green.
Todo List
- Fakes: kv namespace, bot, modules
- cf-kv-store tests (incl.
getJSON/putJSONhappy path + corrupt-JSON swallow) - create-store tests (prefix round-trip, isolation, JSON helpers through prefixing)
- validate-command tests (uniform regex, leading-slash rejection)
- registry tests (load, unified-namespace conflicts, init injection, reset)
- dispatcher tests (every visibility registered via
bot.command()) - help renderer tests (grouping, escaping, private hidden, parse_mode)
- escape-html tests
- All green via
npm test
Success Criteria
npm testpasses with ≥ 95% line coverage on the logic seams (registry, db wrapper, dispatcher, help renderer, validators).- No flaky tests on 5 consecutive runs.
- Unified-namespace conflict detection has dedicated tests covering same-visibility AND cross-visibility collisions.
- Prefix isolation (module A cannot see module B's keys) has a dedicated test.
getJSONcorrupt-JSON swallowing has a dedicated test.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Fake KV diverges from real behavior | Med | Med | Keep fake minimal, aligned to real shape from KV basics report |
vi.mock path resolution differs on Windows |
Med | Low | Use injected-dependency pattern instead — pass moduleRegistry map as a fn param in tests |
| Tests couple to grammY internals | Low | Med | Use fake bot; never import grammY in tests |
| Hidden state in registry module-scope leaks between tests | Med | Med | Export a resetRegistry() test helper; call in beforeEach |
Security Considerations
- Tests must never read real
.dev.varsor hit real KV. Keep everything in-memory.
Next Steps
- Phase 09 documents running the test suite as part of the deploy preflight.