Skip to content
  • Do not abbreviate things like <name> to <n>. This is of the highest priority. Cursor will get this wrong every time and can't recover from it.
  • DO NOT ABBREVIATE <name> to <n> EVER
  • I've added these here because Cursor will go down this rabbit hole of going back and forth on how to signify arguments in help text and then loses track. It takes me longer than I like to manually fix it.
  • When I ask you to write a git commit message, please run git diff --cached | cat to see what's staged and write a message for that change

Coding

  • Reduce the use of conditional logic and prefer pattern matching on functions
  • Prefer pipelines when possible using the |> operator
  • Before coding, make sure we understand the feature at a high-level. It helps to create new documentation while discussing the feature.
  • Try to keep each coding session focused on one thing at a time. It's easy to let things baloon out of control
  • Always minimize work in progress
  • Follow the single responsibility principle when you can. Each module should have a clear responsibility. Consider extracting code to a new module when it becomes confusing.
  • Always fix the warnings before we move on

Documentation

  • After each bit of coding, consider how the documentation needs to be updated. It's very easy for documentation to get stale. Documentation should be ever green, reflecting our latest designs.
  • Try to keep as much implementation logic out of the documentation as possible. Keep the documentation at a high-level.
  • Documentation should be something a human (or AI) can read and understand quickly. Over documenting might be as bad as under documenting.

Data Types

  • All Discord IDs (guild_id, user_id, channel_id, message_id) must be:
  • Defined as :integer in schemas
  • Stored as :bigint in the database
  • This ensures consistency across the codebase and handles Discord's large ID numbers

Testing

Basic TDD

  • Write tests before you write code to demonstrate how you expect the code to work
  • See the tests fail appropriately. That means the error should indicate the missing behavior, not something related to the test set up.
  • Implement only the minimum amount of code to make the test pass. This could mean hard-coding return values.
  • New tests should drive the implementation to be more complete
  • Once all the tests pass, review the code and refactor to be as idiomatic and simple as possible

Specific Practices

  • Avoid mocking when we can. Mocking can lead to a false sense of security. Over mocking can cause tests to pass when the implementation is doing something else.
  • Do not mock code we own. It's better to call it than mock it.
  • Prefer calling actual code to setup test data. Ussing Character.parse/1 to setup characters is much better than trying to manually construct a %Character{} struct. There is a lot of setup code in Character.parse/1 that's import to getting valid data.
  • Dependency injection with test implementations is better than mocking. Multiple airity for functions in elixir should make this relatively easy to implement
  • When you run all the tests with mix test and they pass with no failures and no warnings, there's no need to re-run individual tests
  • Typically, just run the tests for the code we're working on. As long as their are errors or warnings, focus on fixing those and running the specific tests. Run the tests for the whole file. Trying to pick out the specific line number is challenging. After the whole test file runs without failure or warnings, run the whole test suite with mix test.
  • Always generate migrations with mix ecto.gen.migration. This ensures the timestamp in the filename is correct and that migration filenames always increase in value

Test Helpers

We provide several test helper modules to simplify testing external dependencies without mocking. These helpers follow our dependency injection approach and maintain a consistent interface with the real modules.

Message API Helpers

For testing commands that send Discord messages:

  • Stingbatbot.Test.MessageApi - Captures sent messages for verification. Provides functions like get_last_message/1 and clear_messages/0.
  • Stingbatbot.Test.ChannelApi
  • Stingbatbot.Test.ErrorMessageApi - Simulates message sending failures.
  • Stingbatbot.Test.MessageApiHelper - Creates customized message API test implementations.

Example: Instead of mocking the MessageApi, inject our test implementation:

# Pass the test message API to the function
result = Command.execute(context, message_api: MessageApi)
# Verify the message that was sent
assert MessageApi.get_last_message(channel_id) =~ "Expected content"

HTTP Client Helpers

For testing commands that make HTTP requests:

  • Stingbatbot.Test.JSONHTTPClient - Returns configurable JSON responses.
  • Stingbatbot.Test.ErrorHTTPClient - Simulates HTTP request failures.
  • Stingbatbot.Test.StatusHTTPClient - Simulates specific HTTP status codes.
  • Stingbatbot.Test.HTTPClientHelper - Factory for creating HTTP client test implementations.

Game Data Helpers

For testing with consistent game data fixtures:

  • Stingbatbot.Test.WeaponHelper - Creates realistic weapon fixtures for tests. Provides functions to:
  • get_weapon_json_by_name/1: Get the raw JSON data for a weapon from the WeaponStore
  • create_weapon_inventory_item/2: Create a character inventory item with proper weapon structure
  • create_weapon_inventory_items/1: Create multiple weapon inventory items from a list of names

Example: Create test fixtures that match production data structures:

# Create a character with realistic weapons
character_data = %{
  "gear" => WeaponHelper.create_weapon_inventory_items(["Longsword", "Dagger"])
}

# Create a single weapon with a specific instance ID
longsword = WeaponHelper.create_weapon_inventory_item("Longsword", instance_id: "test123")

Using these helpers makes tests more realistic and avoids mocking code we own.

Character Factory

The Stingbatbot.Test.CharacterFactory module provides a consistent way to create character test data. It should be used instead of manually constructing character data structures.

Basic Usage

# Create a basic character
json = CharacterFactory.create_character_json()

# Create a customized character
custom_json = CharacterFactory.create_character_json(
  name: "Gund",
  class: "Fighter",
  level: 3,
  stats: %{"STR" => 16},
  weapons: ["Greatsword", "Dagger"]
)

Class-Specific Factories

Use the class-specific factory functions for characters of specific classes:

# Create a fighter
fighter_json = CharacterFactory.create_fighter_json()

# Create a wizard with spells
wizard_json = CharacterFactory.create_wizard_json(
  spells: ["Fireball", "Magic Missile"]
)

# Create a priest with talents
priest_json = CharacterFactory.create_priest_json(
  talents: [
    %{
      "sourceType" => "Class",
      "sourceName" => "Priest",
      "sourceCategory" => "Talent",
      "name" => "Plus1ToCastingSpells",
      "bonusName" => "Plus1ToCastingSpells",
      "bonusTo" => "Priest",
      "gainedAtLevel" => 3
    }
  ]
)

Best Practices

  1. Use Factory Functions: Always use factory functions instead of manually constructing character data
  2. Start with Base: Use create_character_json() as the base for custom characters
  3. Class-Specific: Use class-specific factories when testing class-specific features
  4. Combine Bonuses: Use the bonuses option to add custom bonuses or talents
  5. Real Data: The factory uses real data from WeaponStore and SpellList, ensuring test data matches production
  6. Use minimal specification: The factory has good defaults. Only pass in attributes that are really important for the specific test. Adding extra attributes makes the tests harder to read and harder to know what's important.

For more examples, see test/stingbatbot/character_factory_example_test.exs.