Ctrl K
ComunidadGuías…

CONTRIBUTING.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Contributing

## Setup

Requires Python 3.13, Node 22, Docker, and [uv](https://docs.astral.sh/uv/).

```bash
uv sync                          # installs the dev dependency group too
docker compose up -d db redis    # Postgres 17 on :5433, Redis on :6380 (see docker-compose.yml)
cp .env.example .env
python -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"  # -> LENOVMAIL_SECRET_KEY
uv run alembic upgrade head
npm --prefix web install
```

Run the API and worker in separate terminals:

```bash
uv run uvicorn lenovmail.api.app:app --reload --port 8080
uv run arq lenovmail.workers.main.WorkerSettings
npm --prefix web run dev         # GUI dev server, proxies API calls to :8080
```

`uv run lenovmail --help` lists the CLI (`bootstrap-admin`, `discover`, `sync`, `agent-token`,
`show-account`, `serve`, `janitor`).

## Checks a PR must pass

Run these before opening a PR; CI (`.github/workflows/ci.yml`) runs the same commands.

```bash
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy
uv run pytest -q
npm --prefix web run typecheck
npm --prefix web run build
```

## Testing policy

- **Unit tests** (`tests/unit/`) that need a database use the `session`/`account`/`folder`
  fixtures in `tests/conftest.py`. Those fixtures call `pytest.skip(...)` — not fail — when
  Postgres isn't reachable, so `uv run pytest -q` without `docker compose up -d db` still runs
  cleanly; it just skips the DB-backed cases.
- **E2E tests** (`tests/e2e/`) exercise a real IMAP/SMTP server (GreenMail) and need:

  ```bash
  docker compose -f docker-compose.test.yml up -d
  uv run pytest -q tests/e2e
  ```

  GreenMail exposes IMAP on `localhost:3143`, IMAPS on `3993`, SMTP on `3025`, SMTPS on `3465`
  (see `docker-compose.test.yml`). It auto-creates a user from the delivery address, so mail
  sent to `demo@localhost` may land in that auto-created mailbox — that's GreenMail behavior,
  not something the test is asserting about Lenovmail.

## Code conventions

These are enforced by `ruff`/`mypy` where mechanical, and expected in review otherwise:

- Every source file (`.py`, `.ts`/`.tsx`, `.css`, `.yml`) starts with a
  `# Lenovmail — authored by satuapps (satuapps.com)` credit line in that language's comment syntax
  (`//` for TypeScript, `/* */` for CSS). Markdown files carry the maintainer line at the
  bottom instead.
- `ruff` line length is 100 (`[tool.ruff]` in `pyproject.toml`); lint rules are
  `E, F, I, UP, B, ASYNC, C4, SIM` with `B008` and `SIM108` ignored.
- Every module opens with `from __future__ import annotations`.
- `mypy` runs with `check_untyped_defs = true` against `src/lenovmail`; new code should type
  clean, not rely on `# type: ignore`.
- Logging goes through `structlog` (`lenovmail.logging.get_logger`). Event names are stable
  identifiers, not sentences — `snake_case`, matched against in tests and dashboards
  (e.g. `agent_audit_failed`, `janitor_account_disabled`). Don't rename an existing event name
  without checking for callers/tests that match on it.
- Comments explain *why*, not *what* — see `crypto.py` or `maintenance.py` for the house style:
  a short module docstring stating the design decision, then code with comments only where the
  reason isn't obvious from reading it.
- New database access goes through the existing session/query patterns in `sync/store.py` and
  `sync/queries.py` rather than ad hoc `select()` calls scattered across callers.

## Commit messages

Single-line, imperative, lower-case subject, no trailing period, optionally scoped by area:

```
janitor: stamp invalid_since on first rejection
api: reject moves into a different account's folder
web: virtualize the message list
```

Keep a commit to one logical change. Reference an issue with `Fixes #123` in the body when
applicable.

## Adding an Alembic migration

Revision ids in this repo are not Alembic's random hex — they're zero-padded sequential
integers (`0001`, `0002`, `0003`, ...), matching the files in `migrations/versions/`.

1. Generate the scaffold:

   ```bash
   uv run alembic revision -m "short description"
   ```

2. Alembic writes a file with a random hex revision id. Rename it to the next sequence number
   (check `migrations/versions/` for the current highest) and edit the generated file so:
   - `revision = "000N"` (the new zero-padded number)
   - `down_revision = "000N-1"` (the previous head)
   - the docstring states *why* the column/table exists, not just what changed — see
     `migrations/versions/0003_account_invalid_since.py` for the expected shape.
3. Write `upgrade()` and `downgrade()` by hand; this project does not use `--autogenerate`.
4. Apply and verify:

   ```bash
   uv run alembic upgrade head
   uv run alembic downgrade -1 && uv run alembic upgrade head   # downgrade round-trips
   ```

## Pull requests

Use the PR template. Keep PRs scoped to one change; unrelated formatting or refactors go in a
separate PR.

---

Maintained by [satuapps](https://satuapps.com).