# Project Memory ## Test Suite Structure - `services/Makefile` runs 7 test suites: lut, utils, bot-core, bot (discord), irc, api, wrapped - Total: ~483 tests (41 lut, 211 utils, 31 bot-core, 67 discord-bot, 33 irc, 65 api, 35 wrapped) - LUT specs: `josie-health-lut/spec/` (fuzzy matching) - bot-core specs: `josie-health-bot-core/spec/` (command_parser, api_contract, dto) - IRC bot specs: `services/irc-bot/spec/` (parity tests) - wrapped-cr specs: `services/wrapped-cr/spec/` (stats_types deserialization, text report) ## Crystal Testing Patterns - `RedisClient` cannot be stubbed via inheritance -- its `initialize` opens a Redis connection and all instance vars must be set - Existing redis_client_spec uses `RedisClient.new rescue nil; next unless client` pattern - For handler tests that need Redis, test the regex + URI.decode_www_form logic directly instead - IRC bot's `irc_bot.cr` has top-level boot code (lines 333+), so it cannot be `require`d in specs -- test constants and logic separately ## IRC Bot Architecture (migrated to bot-core) - IRC bot uses bot-core CommandRouter -- most commands route through `@router.handle()` - IRC-specific (handled locally): ;id multi-dose+alias, ;high, ;insuf/;subl/;inhaled/;buccal/;transdermal, help, verify - Bot-core aliases added: hd, rd, list, hha, note, drugs - `shard.override.yml` points to local paths for dev; `shard.yml` has git deps for CI - Top-level boot code at end of irc_bot.cr (line 298+) prevents `require` in specs - verify_irc_auth uses direct HTTP::Client since it's IRC-specific (not in bot-core APIClient) ## Key Architecture Notes - `josie-health-lut` has route normalization in `drug_lut.cr` line 408 (sniffed, snorted, etc -> insufflated) - `josie-health-lut/src/fuzzy.cr` has Damerau-Levenshtein fuzzy matching with PREFIX_INDEX for substances - Fuzzy thresholds: no fuzzy for <=2 chars, max dist 1 for 3-5, max dist 2 for 6-8, max dist 3 for 9+ - `shard.override.yml` in services/core and services/discord-bot point to local LUT/bot-core for dev - AliasHandler in API (core) uses regex `/\/v1\/aliases\/([^\/]+)\/(.+)/` with `URI.decode_www_form` on captures - TimezoneHandler reads `request.query_params["tz"]` (not "q")