Skip to content

Testing

A developer who is about to change code in ai-sticker-pack or stickers-landing and needs to know which tests exist, how they stay offline, and what must be green before a commit.

  • Product: python -m pytest from the repository root. 888 tests are collected at commit b715418 (python -m pytest --collect-only -q). No keys, no network, no real Telegram.
  • Tests use mocks only. A live AI call is allowed only to look at a picture with your own eyes, with a budget stated for that task and a hard stop at that budget.
  • An automatic fixture in tests/conftest.py neutralises your local bot/.env, so the suite gives the same result on every machine.
  • The kiosk idle timer has a JavaScript test run by node --test; pytest runs it for you and skips it when Node.js is missing.
  • Landing: npm test, npx tsc --noEmit, npm run lint, npm run build — all four before a commit.
  • A hanging check must fail within a minute. Landing tests run with --test-timeout=60000; quality scripts use a watchdog.
Repository Needs
Product Python 3.14 locally (the pinned server requirements target Python 3.12 on the server); a virtualenv with `bot
equirements-dev.txt`; optional Node 22 for the kiosk JS tests. Full setup: Local setup: product
Landing Node 22 or newer; npm install. Full setup: Local setup: landing
pytest.ini testpaths = tests, python_files = test_*.py
tests/
conftest.py shared fixtures, isolation from the local .env, synthetic sheet factory
tg_fake.py fake Telegram session for a real aiogram Bot and Dispatcher
test_*.py 46 test modules
quick_test.py stand-alone AI comparison script (NOT collected by pytest), has --selftest
js/kiosk_idle.test.mjs kiosk idle timer, node:test with mock timers
fixtures/ saved public pack pages (present / absent) and reference prompts

Production code lives in bot/ and is imported exactly as main.py imports it: conftest.py puts bot/ on sys.path.

Terminal window
python -m pytest # everything
python -m pytest tests\test_api.py # one module
python -m pytest -k "leads and not housekeeping" # by keyword
python -m pytest -x -q # stop at the first failure
python tests\quick_test.py --selftest --out out\selftest # offline check of the sheet-cutting script
node --test tests\js\ # kiosk JS test on its own (optional)

quick_test.py --selftest draws a synthetic sheet, cuts it, writes report.html and prints SELFTEST OK. It makes no API call. Every other mode of that script is paid; see Local setup: product.

Family (files) What it protects
Kiosk API — test_api.py, test_kiosk_idle.py, test_messages.py Session → QR → outfit → photo → queued job through FastAPI TestClient; status codes, upload limits, template list order and preview caching; idle reset on the outfit screen (server side plus the JS timer); wording and order of guest messages.
Bot handlers — test_bot_handlers.py, test_admin_stats.py Consent callback; /stats through a real Dispatcher, including the access filter.
Queue and limits — test_db_queue.py, test_pack_limit.py, test_pack_naming.py Session state transitions, queue, one-pack-per-account rule checked atomically, pack name and title rules.
Worker end to end — test_worker_pipeline.py, test_shutdown.py, test_speedups.py The whole pipeline with a fake AI and a fake bot: what is really sent to Telegram; graceful stop and re-queue without losing an attempt; stage timing log lines and safe speed-ups.
Delivery — test_delivery.py The link is sent only after getStickerSet reports 12 stickers and the public pack page confirms twice; the 5-minute notice and the 15-minute failure, both surviving a restart; database migration.
Image processing — test_sticker_builder.py, test_chroma_regression.py, test_layout_regression.py, test_cut_cv.py, test_outline_mode.py, test_template_m1.py Cutting, chroma key, composing on a body template, export to 512 px; regressions from real bad sheets, reproduced synthetically; outline A/B switch; the ready-made body template and the raw-to-template tool.
Figure mode — test_figure_mode.py, test_figure_cut.py, test_render_mode.py Figure sheet prompts, poses and emoji, cutting robust to gaps, attempts and the composite fallback; render mode read from .env (including a byte-order mark), empty value or typo is an error, second main.py refuses to start.
Identity and likeness — test_appearance.py, test_consistency.py, test_likeness_classic.py, test_likeness_hybrid.py, test_skin_zone_flat_cut.py Appearance card schema, identity lock text, QA retries; anchor portrait, skin normalisation, pack QA and targeted regeneration; the classic and hybrid likeness modes; skin shift cap and the flat side-cut detector.
Outfits — test_outfits.py Catalogue entries, preview card format and size (640 × 640 WebP ≤ 70 KB served, 1024 × 1024 source ≤ 250 KB), outfit text in prompts and QA, default outfit, re-choosing before the photo.
Cost and billing — test_usage.py, test_costs.py, test_billing.py Usage recording per call, cost estimate from .env prices, per-job ceiling, daily image counter; the “out of credit” (402) path and maintenance mode.
Admin panel — test_admin_panel.py HTTP Basic, lockout after five failures, no-store / noindex headers, summary, guest table without photos, audited actions, /admin_stats, safe migration of an existing database.
Alerts and config — test_alerts.py, test_config_logging.py, test_event_guard.py, test_storage.py Alert throttling and triggers; monitoring settings and file logging; the paid-tool guard; TTL cleanup.
Leads — test_leads_api.py, test_leads_bot.py, test_leads_commands.py, test_leads_pick.py, test_leads_repeats.py, test_leads_schedule.py, test_leads_housekeeping.py, test_leads_wiring.py Field validation identical to the landing schema, POST /leads, card and status buttons, publishing with retries, group commands and their access rules, picking a lead with buttons, repeat requests within 24 hours, reminder / digest / retention timing with frozen time, the exact dispatcher built by main.build_dispatcher.
Deploy — test_deploy.py, test_deploy_caddy.py Shell syntax and LF line endings of deploy scripts, systemd units, reverse-proxy config, what the push script excludes, strict prod start-up, /health, 429 back-off. Nothing touches a server: stubs replace system commands.
Tools — test_style_compare_pair.py The pair mode of tools/style_compare.py without live calls, including its generation and budget limits.
Fake Where What it replaces
Environment isolation conftest.py, autouse fixture _isolate_from_local_env Overrides the loaded config for every test: empty bot token and AI keys, no alert chat, maintenance off, composite render mode, consistency off, layout 2x6, prices unset, leads without a secret, placeholders hidden, log directory in a temp folder. Your real bot/.env cannot leak into a test.
Public pack page same fixture telegram_stickers.check_tme always answers “present”. Negative scenarios replace it themselves. Polling pauses in delivery are reduced to about a millisecond.
Vision model same fixture ai_pipeline.vision_json raises “vision disabled in tests”. The pipeline must work without it; tests of the appearance card and QA install their own stub.
Image model per test (monkeypatch) Tests replace a pipeline entry point (for example ai_pipeline.generate_head_sheet) or the provider runner with a function that returns a synthetic sheet from conftest.make_sheet() (green background, circle heads). This is the “fake AI”.
Telegram for the worker FakeBot classes inside test modules, e.g. test_worker_pipeline.py Records createNewStickerSet and send_message; asserts that packs are never deleted and that 12 stickers go in one call.
Telegram for handlers tests/tg_fake.py A real aiogram Bot whose session records Bot API requests instead of sending them. Updates are fed through a real Dispatcher (dp.feed_update), so router filters and middleware are exercised. Helpers: make_bot(), message_update(), callback_update().
Time parameters now= / clock= Lead scheduling and rate limiters take the clock as an argument; tests freeze it.

tests/js/kiosk_idle.test.mjs loads bot/kiosk/idle.js and drives it with the mock timers of node:test. No browser is involved. tests/test_kiosk_idle.py finds node on PATH and runs node --test on that file; if Node.js is absent the test is skipped, not failed. Install Node.js before changing idle.js.

Visual and behavioural checks of the whole kiosk page are not part of pytest: run python tools\kiosk_check.py (free, needs Edge or Chrome).

  1. Tests and code checks use mocks only.
  2. A live call exists for one purpose: a human looks at the generated picture.
  3. Each live run has a budget in dollars stated for that task, and stops at it.
  4. One run per check: one face first; more only if the first result is acceptable.
  5. Paid tools use TEST_GEMINI_API_KEY when it is set and refuse to start with EVENT_MODE=1 without it.

The reasons are on Code rules.

  1. python -m pytest — green, and no unexpected skips (check that the kiosk JS test ran if you touched bot/kiosk/).
  2. python tests\quick_test.py --selftest --out out\selftest — prints SELFTEST OK.
  3. If you touched bot/kiosk/: python tools\kiosk_check.py exits with 0.
  4. If you touched templates, outfits or cutting: the matching preview tool from Outfits, templates and art styles.
  5. git status shows no bot/.env, bot/data/, *.db, preview_*.png or out/.
  6. If behaviour, API, environment variables or deployment changed: the matching page of this documentation is updated in the same pull request.
  7. Commit only when asked to; the author comes from the repository’s local git configuration.
Terminal window
npm test # tsx --test --test-timeout=60000 on six test files
npx tsc --noEmit # type check (TypeScript strict)
npm run lint # eslint
npm run build # production build

Requires Node.js 22 or newer.

File Enforces
src/i18n/dictionaries.test.ts ru, uz and en have identical keys and array lengths; no empty string; nothing promises what the product cannot do or names what must not be named; the only numbers are 12, 72 and “3D” (plus the phone sample); Uzbek apostrophes use the correct Unicode letters; a dash never starts a line; each part of the page keeps one form of address; Accept-Language routing; the browser language never selects /uz; the Uzbek publish flag in both positions.
src/content/packs.test.ts Every configured pack has its 12 stickers on disk in both sizes; every outfit has a preview card; the mock kiosk is exactly 3 + 3 with no outfit twice; six demo people with one outfit each; the owner’s pack appears in the hero only; the sticker ribbon uses demo people only, sixteen per row, no repeats.
src/lib/requestSchema.test.ts Form validation and normalisation: required name, Telegram handle and phone; accepted and rejected handle and phone formats; optional fields; length limits; trimming; the layout of the message posted to the group.
src/lib/deliverRequest.test.ts Delivery routing: success on the leads server with the secret header; a duplicate counts as success; timeout, network error, 5xx, 401, 404 → Telegram fallback with a warning line; 429 → throttled, no fallback; 422 → failure, no fallback; both routes failing → failure and the form stays; no leads variables → direct Telegram; no variables at all → console; the secret never reaches the client.
src/lib/requestSent.test.ts The “request sent” flag lasts 24 hours; flags without a valid date are expired; the inline gate before first paint follows the same rule and survives blocked storage.
src/components/hero/scene/steps.test.ts Every step of the hero scene lasts 3–4 s; the stepper has six steps and each scene step belongs to exactly one; one device is in focus per step.

The server-side field rules in bot/services/leads.py mirror requestSchema.ts. The product test tests/test_leads_api.py reuses the same examples, so a change to one side must be made on both. See API reference.

A hang is an error within a minute, not hours of waiting.

  • npm test passes --test-timeout=60000. It catches both a slow test and an endless loop at module import.
  • Each quality script (audit, shots, film, js-weight) runs in steps through scripts/lib/watchdog.mjs: a minute without progress prints the name of the stalled step and exits with code 1. A step that is long on purpose declares its own limit and the reason.
  • A slow machine raises the limit with STEP_TIMEOUT=<ms>; it never removes it.
  • A new check script starts with its own watchdog.
  • When you run checks from a terminal, give each command its own time limit instead of one unlimited chain.
  • On Windows, killing a timed-out command stops only the parent process. next build workers may keep .next/lock, and next start may keep port 3000. Find the owner and stop the whole tree:
Terminal window
Get-NetTCPConnection -LocalPort 3000 | Select-Object OwningProcess
taskkill /PID <pid> /T /F

Visual checks (npm run shots, npm run audit, npm run film, npm run lighthouse) are described on Landing quality tooling.

  1. npx tsc --noEmit
  2. npm run lint
  3. npm test
  4. npm run build
  5. For any visible change: npm run shots and npm run audit against a running server, and look at the screenshots.
  6. No string of page text hard-coded in a component; all three dictionaries updated together.
  7. Commit and push only when asked to; never pass -c user.* to git.

Last verified: 2026-11-13 against ai-sticker-pack@b715418 and ai-stickers-landing@e350ff5.