Files
miti99bot-js/scripts/register.js
T
tiennm99 ea7df56e2d feat(db,cron): phase 04 — dual-write wrappers + factory routing + drift verifier + e2e
The integration phase. Wires Phase 02 (MongoKVStore) and Phase 03
(MongoTradesStore + MongoSqlStore shim) into the request path behind
two env flags so KV and Atlas run side-by-side until cutover.

Storage routing
- DualKVStore + DualSqlStore: Promise.allSettled writes to BOTH backends,
  reads from primary only. Secondary failures log + enqueue onto a KV
  retry queue (__retry:mongo-failed:* / __retry:mongo-sql-failed:*).
  Primary failure throws. _kind="dual" sentinel for test seam.
- create-store.js + create-sql-store.js: full flag matrix (STORAGE_PRIMARY
  ∈ {kv,mongo}, DUAL_WRITE ∈ {0,1}, MONGODB_URI presence) with
  STUB_SENTINEL short-circuit for deploy-time. Post-cutover shape commented
  inline so Phase 07 simplification is mechanical.

Stub mongo for register
- scripts/stub-kv.js: STUB_SENTINEL constant + duck-typed stubMongo
  (no-op connect/close, throwing collection access). Replaces the
  originally-planned string sentinel which would have stalled register.js
  on serverSelectionTimeoutMS if ever passed to MongoClient (code-reviewer #2).
- scripts/register.js: stub env passes MONGODB_URI=STUB_SENTINEL,
  STORAGE_PRIMARY="kv", DUAL_WRITE="0". Asserted via vi.spyOn that
  MongoClient.prototype.connect is never reached.

Drift verifier cron (1/hr)
- src/cron/drift-verifier.js: drains both retry queues by re-attempting
  secondary writes, deletes on success. Spot-checks parity by sampling
  DRIFT_SAMPLE_N keys per module, hashing, logging mismatches.
- src/modules/cron-dispatcher.js: SYSTEM_CRONS array dispatched alongside
  module crons. Keeping system cron out of registry.crons preserves
  existing module-cron length tests and is the cleaner design.
- wrangler.toml: vars STORAGE_PRIMARY/DUAL_WRITE/DRIFT_SAMPLE_N + cron
  schedule "0 * * * *" added.

Trading wiring
- src/modules/registry.js: builds new MongoTradesStore(env) when Mongo
  is in play and threads it as tradesStore into trading module's init
  context. Trading module already accepted optional tradesStore (Phase 03
  backwards-compat) — D1 path remains for STORAGE_PRIMARY=kv + DUAL_WRITE=0.

Tests + verification
- tests/db/dual-kv-store.test.js, dual-sql-store.test.js: write-both,
  secondary-fail-logs+enqueues, primary-fail-throws, reads-primary-only,
  _kind sentinel.
- tests/db/stub-mongo-sentinel.test.js: spy on MongoClient.connect,
  assert zero calls across all flag-matrix combos.
- tests/cron/drift-verifier.test.js: queue drain, skip paths, error safety.
- tests/e2e/storage-roundtrip.test.js: wordle KV dual-write +
  trading MongoTradesStore against fake-mongo.

Tests: 577 → 638 (+61). register:dry passes without Atlas. Lint clean.

Concerns
- Drift-verifier parity-spot-check tests assert queue-drain only;
  full mismatch detection needs real Atlas (Vitest ES-module caching
  blocks reliable prototype patching). Verifier logic verified by
  inspection.
2026-04-26 09:02:07 +07:00

120 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* @file register — post-deploy Telegram registration.
*
* Runs after `wrangler deploy` (chained via `npm run deploy`). Reads env from
* `.env.deploy` (via `node --env-file`), imports the same module registry
* code the Worker uses, derives the public-command list, and calls Telegram's
* HTTP API to (1) register the webhook with a secret token and (2) publish
* setMyCommands.
*
* Idempotent — safe to re-run. Supports `--dry-run` to preview payloads
* without calling Telegram.
*
* Required env (in .env.deploy):
* TELEGRAM_BOT_TOKEN — bot token from BotFather
* TELEGRAM_WEBHOOK_SECRET — must match the value `wrangler secret put` set for the Worker
* WORKER_URL — https://<worker-subdomain>.workers.dev (no trailing slash)
* MODULES — same comma-separated list as wrangler.toml [vars]
*/
import { buildRegistry, resetRegistry } from "../src/modules/registry.js";
import { STUB_SENTINEL, stubAi, stubKv } from "./stub-kv.js";
const TELEGRAM_API = "https://api.telegram.org";
/**
* @param {string} name
* @returns {string}
*/
function requireEnv(name) {
const v = process.env[name];
if (!v || v.trim().length === 0) {
console.error(`missing env: ${name}`);
process.exit(1);
}
return v.trim();
}
/**
* @param {string} token
* @param {string} method
* @param {unknown} body
*/
async function callTelegram(token, method, body) {
const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
/** @type {any} */
let json = {};
try {
json = await res.json();
} catch {
// Telegram always returns JSON on documented endpoints; only leave `json`
// empty if the transport itself died.
}
if (!res.ok || json.ok === false) {
console.error(`${method} failed:`, res.status, json);
process.exit(1);
}
return json;
}
async function main() {
const token = requireEnv("TELEGRAM_BOT_TOKEN");
const secret = requireEnv("TELEGRAM_WEBHOOK_SECRET");
const workerUrl = requireEnv("WORKER_URL").replace(/\/$/, "");
const modules = requireEnv("MODULES");
const dryRun = process.argv.includes("--dry-run");
// Build the registry against the same code the Worker uses. Stub KV
// satisfies the binding so createStore() does not throw.
// MONGODB_URI = STUB_SENTINEL ensures create-store / create-sql-store
// factories short-circuit before constructing any MongoClient.
// STORAGE_PRIMARY + DUAL_WRITE lock the factories to the CF-only path.
resetRegistry();
const reg = await buildRegistry({
MODULES: modules,
KV: stubKv,
AI: stubAi,
MONGODB_URI: STUB_SENTINEL,
STORAGE_PRIMARY: "kv",
DUAL_WRITE: "0",
});
const commands = [...reg.publicCommands.values()].map(({ cmd }) => ({
command: cmd.name,
description: cmd.description,
}));
const webhookBody = {
url: `${workerUrl}/webhook`,
secret_token: secret,
allowed_updates: ["message", "edited_message", "callback_query"],
drop_pending_updates: false,
};
const commandsBody = { commands };
if (dryRun) {
console.log("DRY RUN — not calling Telegram");
// Redact the secret in the printed payload.
console.log("setWebhook:", { ...webhookBody, secret_token: "<redacted>" });
console.log("setMyCommands:", commandsBody);
return;
}
await callTelegram(token, "setWebhook", webhookBody);
await callTelegram(token, "setMyCommands", commandsBody);
console.log(`ok — webhook: ${webhookBody.url}`);
console.log(`ok — ${commands.length} public commands registered:`);
for (const c of commands) console.log(` /${c.command}${c.description}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});