Skip to content

Development Workflow

Detailed guide for developing features and fixing bugs in Genji Shimada.

Setting Up Your Environment

1. Fork the Repository

Click "Fork" on GitHub to create your own copy:

https://github.com/bkan0n/genjishimada

2. Clone Your Fork

git clone https://github.com/YOUR_USERNAME/genjishimada.git
cd genjishimada

3. Add Upstream Remote

git remote add upstream https://github.com/bkan0n/genjishimada.git

4. Install Dependencies

just setup

5. Configure Environment

Copy the local environment template:

cp .env.local.example .env.local

Edit .env.local with your Discord bot token and other settings.

6. Start Infrastructure

docker compose -f docker-compose.local.yml up -d

This starts PostgreSQL and MinIO for local development.

Daily Workflow

1. Sync with Upstream

Before starting work, sync your fork:

git checkout main
git fetch upstream
git merge upstream/main
git push origin main

2. Create a Feature Branch

git checkout -b feature/your-feature-name

Branch naming conventions:

  • feature/ - New features
  • fix/ - Bug fixes
  • docs/ - Documentation changes
  • refactor/ - Code refactoring
  • test/ - Test improvements

3. Make Changes

Edit code, add tests, update documentation.

4. Run Linters

just lint-all

This runs:

  • Ruff (formatting and linting)
  • BasedPyright (type checking)

Fix linting issues:

# Format code
uv run ruff format .

# Auto-fix linting issues
uv run ruff check --fix .

5. Run Tests

just test-api
just test-queue

For faster iteration, run specific tests:

# API tests only
just test-api

# Specific test file
just test-api tests/maps/test_maps_api.py

# Specific test function
just test-api tests/maps/test_maps_api.py::TestSearchMaps::test_happy_path

6. Commit Changes

Write clear, descriptive commit messages:

git add .
git commit -m "feat: add map search endpoint

- Add /maps/search route with query parameters
- Implement full-text search using PostgreSQL
- Add tests for search functionality
"

Commit message format:

<type>: <short description>

<detailed description>

<footer (optional)>

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation
  • refactor - Code refactoring
  • test - Test improvements
  • chore - Maintenance tasks

7. Push to Your Fork

git push origin feature/your-feature-name

8. Open a Pull Request

  1. Go to your fork on GitHub
  2. Click "Compare & pull request"
  3. Fill out the PR template
  4. Submit the PR

Working on the API

Project Structure

apps/api/
├── di/                # Business logic (DI modules)
│   ├── base.py        # BaseService with PostgreSQL queue publishing
│   ├── auth.py        # Authentication logic
│   ├── maps.py        # Map CRUD operations
│   └── completions.py # Completion tracking
├── routes/            # HTTP route handlers
│   ├── maps/          # Map-related routes
│   └── completions.py # Completion endpoints
├── middleware/        # Auth and request processing
│   ├── auth.py        # CustomAuthenticationMiddleware
│   └── guards.py      # Scope guards
├── utilities/         # Shared utilities
│   └── errors.py      # Custom exceptions
└── tests/             # API tests

Adding a New Endpoint

  1. Define the route handler in routes/:
from litestar import get
from genjishimada_sdk.maps import MapResponse

@get("/maps/{map_id:int}")
async def get_map(map_id: int, conn: Connection) -> MapResponse:
    map_data = await maps_service.get_map(conn, map_id)
    return map_data
  1. Implement business logic in di/:
from asyncpg import Connection
from genjishimada_sdk.maps import MapResponse

async def get_map(conn: Connection, map_id: int) -> MapResponse:
    row = await conn.fetchrow(
        "SELECT * FROM maps.maps WHERE id = $1",
        map_id
    )
    if not row:
        raise CustomHTTPException(404, "Map not found")
    return MapResponse(**row)
  1. Add tests in tests/:
import pytest
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_get_map(client: AsyncClient):
    response = await client.get("/api/v3/maps/1")
    assert response.status_code == 200
  1. Run tests:
just test-api

Database Queries

Use AsyncPG with parameterized queries:

# Good - parameterized query
row = await conn.fetchrow(
    "SELECT * FROM maps.maps WHERE id = $1",
    map_id
)

# Bad - SQL injection risk
row = await conn.fetchrow(
    f"SELECT * FROM maps.maps WHERE id = {map_id}"
)

Enqueueing background work

Persist domain changes and their queue jobs on one connection within one transaction. Construct shared SDK events and call genjishimada_sdk.queue_store.enqueue_job with a durable business event_key and entity identity. A helper must not open or commit a separate connection for publication.

A retry of the same operation resolves the existing public job. Distinct legitimate transitions use new persisted operation identities. Worker handlers receive JobContext, record effects individually, and keep completed receipts when retrying. See queue operations and the SDK examples.

Working on the Bot

Project Structure

apps/bot/
├── core/
│   └── genji.py       # Main bot class
├── extensions/        # Feature modules (cogs)
│   ├── queue.py      # PostgreSQL queue service
│   ├── api_service.py # API client
│   └── completions.py # Completion handlers
└── configs/           # Configuration files
    ├── dev.toml
    └── prod.toml

Adding a Queue Consumer

  1. Define the consumer in an extension:
from extensions._queue_registry import queue_consumer
from genjishimada_sdk.completions import CompletionCreatedEvent

@queue_consumer(
    "api.completion.submission",
    struct_type=CompletionCreatedEvent,
    idempotent=True
)
async def handle_completion(
    self,
    event: CompletionCreatedEvent,
    message: JobContext,
) -> None:
    # Send Discord notification
    channel = self.bot.get_channel(COMPLETION_CHANNEL_ID)
    await channel.send(f"New completion: {event.completion_id}")
  1. Test manually:

Trigger the event from the API and verify the bot processes it.

Calling the API

Use the shared APIService attached to the bot:

async def get_user(user_id: int):
    return await bot.api.get_user(user_id)

Working on the SDK

Adding a New Model

  1. Define the model in libs/sdk/src/genjishimada_sdk/:
import msgspec

class Achievement(msgspec.Struct):
    id: int
    name: str
    description: str
    icon_url: str | None
    xp_reward: int
  1. Export from __init__.py:
from .achievements import Achievement

__all__ = ["Achievement", ...]
  1. Use in API and bot:
from genjishimada_sdk import Achievement

achievement = Achievement(
    id=1,
    name="First Completion",
    description="Complete your first map",
    icon_url=None,
    xp_reward=100,
)
  1. Run linters:
just lint-sdk

Debugging

API Debugging

  1. Add debug logging:
import logging

logger = logging.getLogger(__name__)
logger.debug(f"Processing completion: {completion_id}")
  1. Run with debug mode:
cd apps/api
uv run litestar run --debug
  1. Check logs:

Logs appear in the terminal.

Bot Debugging

  1. Add debug logging:
import logging

logger = logging.getLogger(__name__)
logger.debug(f"Received event: {event}")
  1. Run the bot:
just run-bot
  1. Check logs:

Logs appear in the terminal.

Database Debugging

  1. Connect to PostgreSQL:
docker exec -it genjishimada-db-local psql -U genji -d genjishimada
  1. Run queries:
SELECT * FROM maps.maps LIMIT 10;

Common Issues

Import Errors

If you see import errors, re-sync dependencies:

just sync

Database Connection Failed

Ensure Docker services are running:

docker compose -f docker-compose.local.yml ps

Tests Failing

Run tests with verbose output:

just test-api --workers 0 -vv

Type Errors

Run BasedPyright to see all type errors:

uv run basedpyright apps/api

Next Steps