mirror of
https://github.com/tiennm99/miti99bot.git
synced 2026-05-02 18:21:18 +00:00
chore: project cleanup — purge stale function-calling refs + sync docs
Followed code-reviewer audit. Findings applied: - twentyq/README.md, twentyq/index.js header — claimed "function calling" + ANSWER_FUNCTION_SCHEMA / submit_answer; rewrite to JSON-in-content matching what the code actually does. Added generateRoundStart line. - wrangler.toml [ai] comment — list both bge-m3 (semantle/doantu) AND gemma-4 (twentyq) consumers; drop neuron math that no longer matched. - scripts/stub-kv.js — drop reference to nonexistent REGISTER_DRYRUN flag. - twentyq/ai-client.redactSecret — strip dead "if (out.length > 0)" branch (String.replace cannot produce empty string from the inputs we pass). - handlers.test.js — drop noise saveGame() before "no games" stats assert; add ai.run call-count guards on two-AI-call flows. - docs/codebase-summary.md — full rewrite of Active Modules table (semantle/doantu/lolschedule/twentyq were missing); fix vitest 2→4 + wrangler 3→4 versions; replace stale 200-test count with current ~450. - docs/architecture.md — file tree includes lolschedule/semantle/doantu/ twentyq + cron-dispatcher + sql-store* + scripts/migrate.js; moduleRegistry snippet matches src/modules/index.js. - docs/todo.md — entire file obsolete (D1 UUID populated, cron live). Deleted. Tests: 449 pass, lint clean.
This commit is contained in:
+29
-15
@@ -17,28 +17,38 @@ For authoring a new plugin module, see [`adding-a-module.md`](./adding-a-module.
|
|||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── index.js ── fetch router: POST /webhook + GET / health
|
├── index.js ── fetch + scheduled handlers
|
||||||
├── bot.js ── memoized grammY Bot factory, lazy dispatcher install
|
├── bot.js ── memoized grammY Bot factory, lazy dispatcher install
|
||||||
├── db/
|
├── db/
|
||||||
│ ├── kv-store-interface.js ── JSDoc typedefs only — the contract
|
│ ├── kv-store-interface.js ── KVStore contract (JSDoc)
|
||||||
│ ├── cf-kv-store.js ── Cloudflare KV adapter
|
│ ├── cf-kv-store.js ── Cloudflare KV adapter
|
||||||
│ └── create-store.js ── per-module prefixing factory
|
│ ├── create-store.js ── KV per-module prefixing factory
|
||||||
|
│ ├── sql-store-interface.js ── SqlStore contract (JSDoc)
|
||||||
|
│ ├── cf-sql-store.js ── Cloudflare D1 adapter
|
||||||
|
│ └── create-sql-store.js ── D1 per-module prefixing factory
|
||||||
├── modules/
|
├── modules/
|
||||||
│ ├── index.js ── static import map (add new modules here)
|
│ ├── index.js ── static import map (add new modules here)
|
||||||
│ ├── registry.js ── loader + builder + conflict detection + memoization
|
│ ├── registry.js ── loader + builder + conflict detection + memoization
|
||||||
│ ├── dispatcher.js ── bot.command() for every visibility
|
│ ├── dispatcher.js ── bot.command() for every visibility
|
||||||
│ ├── validate-command.js ── shared validators
|
│ ├── cron-dispatcher.js ── routes scheduled events to matching module crons
|
||||||
│ ├── util/ ── fully implemented: /info + /help
|
│ ├── validate-command.js ── command contract validator
|
||||||
│ ├── trading/ ── paper trading: VN stocks (dynamic symbol resolution)
|
│ ├── validate-cron.js ── cron contract validator
|
||||||
│ ├── wordle/ ── 5-letter guessing game (KV storage)
|
│ ├── util/ ── /info + /help
|
||||||
│ ├── loldle/ ── classic-mode LoL champion guesser (KV storage)
|
│ ├── misc/ ── stub: /ping + /mstats
|
||||||
│ └── misc/ ── stub that exercises the DB (ping/mstats)
|
│ ├── trading/ ── paper trading: VN stocks (D1 + KV, daily cron)
|
||||||
|
│ ├── wordle/ ── 5-letter guessing game (KV)
|
||||||
|
│ ├── loldle/ ── classic-mode LoL champion guesser (KV)
|
||||||
|
│ ├── lolschedule/ ── LoL esports schedule + daily digest subscriptions (KV, cron)
|
||||||
|
│ ├── semantle/ ── English semantic word guess (KV, word2sim)
|
||||||
|
│ ├── doantu/ ── Vietnamese semantle (KV, phow2sim)
|
||||||
|
│ └── twentyq/ ── reverse-Akinator yes/no game (KV + Workers AI)
|
||||||
└── util/
|
└── util/
|
||||||
└── escape-html.js
|
└── escape-html.js
|
||||||
|
|
||||||
scripts/
|
scripts/
|
||||||
├── register.js ── post-deploy: setWebhook + setMyCommands
|
├── register.js ── post-deploy: setWebhook + setMyCommands
|
||||||
└── stub-kv.js ── no-op KV binding for deploy-time registry build
|
├── migrate.js ── apply D1 migrations
|
||||||
|
└── stub-kv.js ── no-op KV + AI bindings for deploy-time registry build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Cold-start and the bot factory
|
## 3. Cold-start and the bot factory
|
||||||
@@ -104,11 +114,15 @@ Cloudflare Workers bundle statically via wrangler. A dynamic import from a varia
|
|||||||
```js
|
```js
|
||||||
// src/modules/index.js
|
// src/modules/index.js
|
||||||
export const moduleRegistry = {
|
export const moduleRegistry = {
|
||||||
util: () => import("./util/index.js"),
|
util: () => import("./util/index.js"),
|
||||||
wordle: () => import("./wordle/index.js"),
|
wordle: () => import("./wordle/index.js"),
|
||||||
loldle: () => import("./loldle/index.js"),
|
loldle: () => import("./loldle/index.js"),
|
||||||
misc: () => import("./misc/index.js"),
|
misc: () => import("./misc/index.js"),
|
||||||
trading: () => import("./trading/index.js"),
|
trading: () => import("./trading/index.js"),
|
||||||
|
lolschedule: () => import("./lolschedule/index.js"),
|
||||||
|
semantle: () => import("./semantle/index.js"),
|
||||||
|
doantu: () => import("./doantu/index.js"),
|
||||||
|
twentyq: () => import("./twentyq/index.js"),
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+18
-24
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Telegram bot on Cloudflare Workers with a plug-n-play module system. grammY handles Telegram API; modules register commands with three visibility levels. Data stored in Cloudflare KV behind a prefixed `KVStore` interface.
|
Telegram bot on Cloudflare Workers with a plug-n-play module system. grammY handles Telegram API; modules register commands with three visibility levels. Data stored in Cloudflare KV (behind a prefixed `KVStore` interface) or D1 (behind `SqlStore` interface).
|
||||||
|
|
||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
@@ -10,21 +10,25 @@ Telegram bot on Cloudflare Workers with a plug-n-play module system. grammY hand
|
|||||||
|-------|-----------|
|
|-------|-----------|
|
||||||
| Runtime | Cloudflare Workers (V8 isolates) |
|
| Runtime | Cloudflare Workers (V8 isolates) |
|
||||||
| Bot framework | grammY 1.x |
|
| Bot framework | grammY 1.x |
|
||||||
| Storage | Cloudflare KV |
|
| Storage | Cloudflare KV + D1 |
|
||||||
|
| AI inference | Workers AI binding (`env.AI`) |
|
||||||
| Linter/Formatter | Biome |
|
| Linter/Formatter | Biome |
|
||||||
| Tests | Vitest |
|
| Tests | Vitest |
|
||||||
| Deploy | Wrangler CLI |
|
| Deploy | Wrangler CLI |
|
||||||
|
|
||||||
## Active Modules
|
## Active Modules
|
||||||
|
|
||||||
| Module | Status | Commands | Storage | Crons | Description |
|
| Module | Commands | Storage | Crons | Description |
|
||||||
|--------|--------|----------|---------|-------|-------------|
|
|--------|----------|---------|-------|-------------|
|
||||||
| `util` | Complete | `/info`, `/help`, `/stickerid` (private) | — | — | Bot info, command help renderer, and sticker file_id echo helper |
|
| `util` | `/info`, `/help`, `/stickerid` (private) | — | — | Bot info, command help renderer, sticker file_id echo helper |
|
||||||
| `trading` | Complete | `/trade_topup`, `/trade_buy`, `/trade_sell`, `/trade_convert`, `/trade_stats`, `/history` | D1 (trades) + KV (portfolio, symbol cache) | Daily 5PM trim | Paper trading — VN stocks with dynamic symbol resolution. Crypto/gold/forex coming soon. |
|
| `misc` | `/ping`, `/mstats`, `/fortytwo` | KV | — | Health check + DB demo stub |
|
||||||
| `wordle` | Complete | `/wordle`, `/wordle_new`, `/wordle_giveup`, `/wordle_stats` | KV (game, stats) | — | Classic 5-letter word game. 14,855-word dict sourced from [dracos's gist](https://gist.github.com/dracos/dd0668f281e685bad51479e5acaadb93). |
|
| `trading` | `/trade_topup`, `/trade_buy`, `/trade_sell`, `/trade_convert`, `/trade_stats`, `/history` | D1 (trades) + KV (portfolio, symbol cache) | Daily 5PM trim | Paper trading — VN stocks with dynamic symbol resolution |
|
||||||
| `loldle` | Complete | `/loldle`, `/loldle_giveup`, `/loldle_stats` | KV (game, stats) | — | Classic-mode LoL champion guesser (auto-starts a new round after solve/giveup). Champion data synced from `tiennm99/loldle-data`. |
|
| `wordle` | `/wordle`, `/wordle_new`, `/wordle_giveup`, `/wordle_stats` | KV | — | 5-letter word guessing game. 14,855-word dict |
|
||||||
| `twentyq` | Complete | `/twentyq`, `/twentyq_giveup`, `/twentyq_stats` | KV (game, stats) | — | Reverse-Akinator yes/no game. Workers AI (`@cf/google/gemma-4-26b-a4b-it`) judges each question via function calling + generates fresh hints. |
|
| `loldle` | `/loldle`, `/loldle_giveup`, `/loldle_stats` | KV | — | Classic-mode LoL champion guesser. Data synced from `tiennm99/loldle-data` |
|
||||||
| `misc` | Stub | `/ping`, `/mstats`, `/fortytwo` | KV | — | Health check + DB demo |
|
| `lolschedule` | `/lolschedule_today`, `/lolschedule_week`, `/lolschedule_subscribe`, `/lolschedule_unsubscribe` | KV | Daily 01:00 UTC | LoL esports schedule + daily digest subscriptions |
|
||||||
|
| `semantle` | `/semantle`, `/semantle_giveup`, `/semantle_stats` | KV | — | English semantic word guessing via hosted word2sim service |
|
||||||
|
| `doantu` | `/doantu`, `/doantu_hint`, `/doantu_giveup`, `/doantu_stats` | KV | — | Vietnamese semantle via hosted phow2sim service |
|
||||||
|
| `twentyq` | `/twentyq`, `/twentyq_giveup`, `/twentyq_stats` | KV | — | Reverse-Akinator yes/no game. Workers AI (`@cf/google/gemma-4-26b-a4b-it`) generates round-start category+hint and judges each turn via one-line JSON |
|
||||||
|
|
||||||
## Key Data Flows
|
## Key Data Flows
|
||||||
|
|
||||||
@@ -62,23 +66,13 @@ npm run deploy
|
|||||||
|-----------|---------|---------|
|
|-----------|---------|---------|
|
||||||
| `grammy` | Telegram Bot API framework | ^1.30.0 |
|
| `grammy` | Telegram Bot API framework | ^1.30.0 |
|
||||||
| `@biomejs/biome` | Linting + formatting (dev) | ^1.9.0 |
|
| `@biomejs/biome` | Linting + formatting (dev) | ^1.9.0 |
|
||||||
| `vitest` | Test runner (dev) | ^2.1.0 |
|
| `vitest` | Test runner (dev) | ^4.1.4 |
|
||||||
| `wrangler` | Cloudflare Workers CLI (dev) | ^3.90.0 |
|
| `wrangler` | Cloudflare Workers CLI (dev) | ^4.84.0 |
|
||||||
|
|
||||||
## Module Documentation
|
## Module Documentation
|
||||||
|
|
||||||
Each module maintains its own `README.md` with commands, data model, and implementation details. See `src/modules/<name>/README.md`.
|
Each module maintains its own `README.md` with commands, data model, and implementation details. See `src/modules/<name>/README.md`.
|
||||||
|
|
||||||
## Test Coverage
|
## Tests
|
||||||
|
|
||||||
200 tests across 21 test files (run via `npm test` — ~2s):
|
`npm test` runs the full vitest suite (run in a few seconds — ~450 tests). Structure: one folder per module under `tests/modules/<name>/`, shared fakes under `tests/fakes/` (fake-kv-namespace, fake-d1, fake-bot, fake-modules, fake-ai). No workerd, no Telegram fixtures — pure-logic unit tests with injected fakes.
|
||||||
|
|
||||||
| Area | Tests | What's Covered |
|
|
||||||
|------|-------|---------------|
|
|
||||||
| DB layer (KV) | 19 | KV store, prefixing, JSON helpers, pagination |
|
|
||||||
| DB layer (D1) | — | Fake D1 in-memory implementation (fake-d1.js) backs trading tests |
|
|
||||||
| Module framework | 33 | Registry, dispatcher, validators, help renderer, cron validation |
|
|
||||||
| Utilities | 4 | HTML escaping |
|
|
||||||
| Trading module | 79 | Symbol resolution, formatters, flat portfolio CRUD, command handlers, history/retention |
|
|
||||||
| Loldle module | 18 | Classic-mode champion comparison, champion lookup, daily picker |
|
|
||||||
| Wordle module | 13 | Duplicate-letter two-pass comparison, guess validation |
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
# TODO
|
|
||||||
|
|
||||||
Manual follow-ups after the D1 + Cron infra rollout (plan: `plans/260415-1010-d1-cron-infra/`).
|
|
||||||
|
|
||||||
## Pre-deploy (required before next `npm run deploy`)
|
|
||||||
|
|
||||||
- [ ] Create the D1 database:
|
|
||||||
```bash
|
|
||||||
npx wrangler d1 create miti99bot-db
|
|
||||||
```
|
|
||||||
Copy the returned UUID.
|
|
||||||
|
|
||||||
- [ ] Replace `REPLACE_ME_D1_UUID` in `wrangler.toml` (`[[d1_databases]]` → `database_id`) with the real UUID.
|
|
||||||
|
|
||||||
- [ ] Commit `wrangler.toml` with the real UUID (the ID is not a secret).
|
|
||||||
|
|
||||||
## First deploy verification
|
|
||||||
|
|
||||||
- [ ] Run `npm run db:migrate -- --dry-run` — confirm it lists `src/modules/trading/migrations/0001_trades.sql` as pending.
|
|
||||||
|
|
||||||
- [ ] Run `npm run deploy` — chain is `wrangler deploy` → `npm run db:migrate` → `npm run register`.
|
|
||||||
|
|
||||||
- [ ] Verify in Cloudflare dashboard:
|
|
||||||
- D1 database `miti99bot-db` shows `trading_trades` + `_migrations` tables
|
|
||||||
- Worker shows a cron trigger `0 17 * * *`
|
|
||||||
|
|
||||||
## Post-deploy smoke tests
|
|
||||||
|
|
||||||
- [ ] Send `/buy VNM 10 80000` (or whatever the real buy syntax is) via Telegram, then `/history` — expect 1 row.
|
|
||||||
|
|
||||||
- [ ] Manually fire the cron to verify retention:
|
|
||||||
```bash
|
|
||||||
npx wrangler dev --test-scheduled
|
|
||||||
# in another terminal:
|
|
||||||
curl "http://localhost:8787/__scheduled?cron=0+17+*+*+*"
|
|
||||||
```
|
|
||||||
Check logs for `trim-trades` output.
|
|
||||||
|
|
||||||
## Nice-to-have (not blocking)
|
|
||||||
|
|
||||||
- [ ] End-to-end test of `wrangler dev --test-scheduled` documented with real output snippet in `docs/using-cron.md`.
|
|
||||||
|
|
||||||
- [ ] Decide on migration rollback story (currently forward-only). Either document "write a new migration to undo" explicitly, or add a `down/` convention.
|
|
||||||
|
|
||||||
- [ ] Tune `trim-trades` schedule if 17:00 UTC conflicts with anything — currently chosen as ~00:00 ICT.
|
|
||||||
|
|
||||||
- [ ] Consider per-environment D1 (staging vs prod) if a staging bot is added later.
|
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
* These stubs satisfy the shape without doing any real IO. All init hooks
|
* These stubs satisfy the shape without doing any real IO. All init hooks
|
||||||
* are assumed read-only (or tolerant of missing state) at registration time.
|
* are assumed read-only (or tolerant of missing state) at registration time.
|
||||||
* If a future module writes inside init(), update the matching stub to
|
* If a future module writes inside init(), update the matching stub to
|
||||||
* swallow writes or gate the write on a `process.env.REGISTER_DRYRUN` flag.
|
* swallow writes safely.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {KVNamespace} */
|
/** @type {KVNamespace} */
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# Twentyq Module
|
# Twentyq Module
|
||||||
|
|
||||||
A reverse-Akinator yes/no guessing game. The bot picks a secret object from a
|
A reverse-Akinator yes/no guessing game. The bot picks a secret keyword from
|
||||||
hand-curated seed list, gives an opening hint, then judges every user input
|
a flat seed list. At round-start, a Workers AI LLM
|
||||||
with a Workers AI LLM (`@cf/google/gemma-4-26b-a4b-it`) via function calling.
|
(`@cf/google/gemma-4-26b-a4b-it`) generates a category + cryptic opening
|
||||||
Each turn the model returns `{ is_guess, answer, hint }`. Round ends on a
|
hint. Each turn the same model judges the user's input and returns
|
||||||
correct guess (`is it an organ?` matches secret) or `/twentyq_giveup`.
|
`{ is_guess, answer, hint }` as one-line JSON (parsed out of response text —
|
||||||
|
no function calling, no tools array). Round ends on a correct guess or
|
||||||
|
`/twentyq_giveup`.
|
||||||
|
|
||||||
**Visibility: `public`** — commands appear in both `/help` and Telegram's
|
**Visibility: `public`** — commands appear in both `/help` and Telegram's
|
||||||
native `/` autocomplete menu.
|
native `/` autocomplete menu.
|
||||||
@@ -60,16 +62,19 @@ AI request) + one request per turn, well under the cap for normal play volume.
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
- `seeds.js` — `SEEDS` array + `getRandomSeed(rng)`. Targets are lowercased.
|
- `seeds.js` — flat `SEEDS` string array of target keywords + `getRandomSeed(rng)`.
|
||||||
- `state.js` — KV persistence for game + stats. Subject = user id (DM) or
|
- `state.js` — KV persistence for game + stats. Subject = user id (DM) or
|
||||||
chat id (group). 7-day TTL on the active round.
|
chat id (group). 7-day TTL on the active round.
|
||||||
- `prompts.js` — `buildSystemPrompt(state)` injects secret + history;
|
- `prompts.js` — `buildStartRoundPrompt(target)` (opens a round) and
|
||||||
`ANSWER_FUNCTION_SCHEMA` declares the `submit_answer` tool.
|
`buildSystemPrompt(state)` (per-turn judging). Both include HINT STYLE
|
||||||
|
rules to keep hints cryptic.
|
||||||
- `validate-input.js` — pre-AI regex check; rejects open-ended starters,
|
- `validate-input.js` — pre-AI regex check; rejects open-ended starters,
|
||||||
empty/oversized input. Saves Neurons.
|
empty/oversized input. Saves Neurons.
|
||||||
- `ai-client.js` — wraps `env.AI.run`, parses both Cloudflare-traditional and
|
- `ai-client.js` — wraps `env.AI.run`. `generateRoundStart(env, target)`
|
||||||
OpenAI-style tool-call shapes, normalizes payload, redacts the secret from
|
produces `{category, initialHint}`; `judge(env, state, userInput)` produces
|
||||||
hints (defense-in-depth). `UpstreamError` wraps any failure.
|
`{is_guess, answer, hint}`. Both parse one-line JSON from response text,
|
||||||
|
redact the secret word from any generated hint, and throw `UpstreamError`
|
||||||
|
on upstream failure.
|
||||||
- `render.js` — five Telegram-HTML formatters; all user-derived text
|
- `render.js` — five Telegram-HTML formatters; all user-derived text
|
||||||
HTML-escaped.
|
HTML-escaped.
|
||||||
- `handlers.js` — three command entry points + subject resolver + repeat
|
- `handlers.js` — three command entry points + subject resolver + repeat
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ export function redactSecret(hint, target) {
|
|||||||
if (!target) return hint;
|
if (!target) return hint;
|
||||||
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const re = new RegExp(`\\b${escaped}\\b`, "ig");
|
const re = new RegExp(`\\b${escaped}\\b`, "ig");
|
||||||
const out = hint.replace(re, "(redacted)");
|
return hint.replace(re, "(redacted)");
|
||||||
return out.length > 0 ? out : "the hint was redacted to avoid revealing the answer";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* @file Twentyq module — reverse-Akinator yes/no guessing game.
|
* @file Twentyq module — reverse-Akinator yes/no guessing game.
|
||||||
*
|
*
|
||||||
* Bot picks a secret object from a hand-curated seed list (./seeds.js) and
|
* Bot picks a secret keyword from ./seeds.js. Workers AI
|
||||||
* gives an initial hint. Each user input is judged by Workers AI
|
* (@cf/google/gemma-4-26b-a4b-it) generates {category, initialHint} at
|
||||||
* (@cf/google/gemma-4-26b-a4b-it) via function calling — the model returns
|
* round start and emits a one-line JSON {is_guess, answer, hint} per turn.
|
||||||
* { is_guess, answer, hint }. Round ends on a correct guess or /twentyq_giveup.
|
* Round ends on a correct guess or /twentyq_giveup. Unlimited turns.
|
||||||
* Unlimited turns. Per-subject state in KV (user id in DMs, chat id in groups).
|
* Per-subject state in KV (user id in DMs, chat id in groups).
|
||||||
*
|
*
|
||||||
* `init` captures both the prefixed KV store AND the raw env so handlers can
|
* `init` captures both the prefixed KV store AND the raw env so handlers can
|
||||||
* reach env.AI per request without changing the dispatcher contract.
|
* reach env.AI per request without changing the dispatcher contract.
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ describe("twentyq/handlers", () => {
|
|||||||
mockJudgement(ai, { is_guess: false, answer: "yes", hint: "yes hint" });
|
mockJudgement(ai, { is_guess: false, answer: "yes", hint: "yes hint" });
|
||||||
const ctx = makeCtx(1, "private", "/twentyq is it big?");
|
const ctx = makeCtx(1, "private", "/twentyq is it big?");
|
||||||
await handleTwentyq(ctx, { db, env });
|
await handleTwentyq(ctx, { db, env });
|
||||||
|
expect(ai.run).toHaveBeenCalledTimes(2); // roundstart + judge
|
||||||
expect(ctx.reply).toHaveBeenCalledTimes(2); // intro + turn
|
expect(ctx.reply).toHaveBeenCalledTimes(2); // intro + turn
|
||||||
expect(ctx.replies[0].text).toMatch(/I'm thinking/);
|
expect(ctx.replies[0].text).toMatch(/I'm thinking/);
|
||||||
expect(ctx.replies[1].text).toMatch(/Yes/);
|
expect(ctx.replies[1].text).toMatch(/Yes/);
|
||||||
@@ -164,6 +165,7 @@ describe("twentyq/handlers", () => {
|
|||||||
const ctx = makeCtx(99, "group", "/twentyq is it big?");
|
const ctx = makeCtx(99, "group", "/twentyq is it big?");
|
||||||
ctx.chat.id = 12345;
|
ctx.chat.id = 12345;
|
||||||
await handleTwentyq(ctx, { db, env });
|
await handleTwentyq(ctx, { db, env });
|
||||||
|
expect(ai.run).toHaveBeenCalledTimes(2); // roundstart + judge
|
||||||
// Game saved under chat id (12345), not user id (99)
|
// Game saved under chat id (12345), not user id (99)
|
||||||
expect(await loadGame(db, 12345)).not.toBeNull();
|
expect(await loadGame(db, 12345)).not.toBeNull();
|
||||||
expect(await loadGame(db, 99)).toBeNull();
|
expect(await loadGame(db, 99)).toBeNull();
|
||||||
@@ -190,11 +192,9 @@ describe("twentyq/handlers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("handleStats", () => {
|
describe("handleStats", () => {
|
||||||
it("renders stats summary", async () => {
|
it("renders empty-stats message when no rounds finished", async () => {
|
||||||
await saveGame(db, 1, sampleGame());
|
|
||||||
const ctx = makeCtx(1);
|
const ctx = makeCtx(1);
|
||||||
await handleStats(ctx, { db });
|
await handleStats(ctx, { db });
|
||||||
// No games played yet -> "no twentyq games"
|
|
||||||
expect(ctx.replies[0].text).toMatch(/no.*games/i);
|
expect(ctx.replies[0].text).toMatch(/no.*games/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-6
@@ -25,12 +25,11 @@ binding = "DB"
|
|||||||
database_name = "miti99bot-db"
|
database_name = "miti99bot-db"
|
||||||
database_id = "261b54e7-0fdb-4fe7-8ed9-2e8a8bcf459c"
|
database_id = "261b54e7-0fdb-4fe7-8ed9-2e8a8bcf459c"
|
||||||
|
|
||||||
# Workers AI — inference binding used by semantle + doantu for
|
# Workers AI inference binding, accessed as `env.AI` in handlers.
|
||||||
# @cf/baai/bge-m3 multilingual text embeddings. Accessed as `env.AI`
|
# Used by:
|
||||||
# in handlers. Included on the Workers Free plan: 10,000 Neurons/day at
|
# - semantle / doantu → @cf/baai/bge-m3 multilingual embeddings
|
||||||
# no charge (hard-stops — no billing on Free plan).
|
# - twentyq → @cf/google/gemma-4-26b-a4b-it text generation
|
||||||
# bge-m3 is 1075 Neurons per M input tokens → ~0.002 N/guess (2 short
|
# Workers Free plan: 10,000 Neurons/day, hard-stops (no billing on Free).
|
||||||
# words), ~4.6M guesses/day within the cap.
|
|
||||||
# Pricing: https://developers.cloudflare.com/workers-ai/platform/pricing/
|
# Pricing: https://developers.cloudflare.com/workers-ai/platform/pricing/
|
||||||
[ai]
|
[ai]
|
||||||
binding = "AI"
|
binding = "AI"
|
||||||
|
|||||||
Reference in New Issue
Block a user