# Sviluppo Setup locale, pytest, ruff, e come estendere il client. --- ## Setup ```bash git clone https://192.168.0.183:8085/truetype/viandante.git cd viandante python3 -m venv .venv source .venv/bin/activate pip install -e ".[dev]" ``` Lo sviluppo quotidiano è Python puro; nessun build system esterno (setuptools è trasparente via `pyproject.toml`). --- ## Test ```bash pytest # 66+ test (telnet + ansi + deadsouls + mapper + logger) pytest tests/test_logger.py -v # solo il logger (più verboso) pytest -k "test_rolling_parser" # filtrato per nome pytest --tb=short # traceback corti ``` I test usano `pytest.ini_options = ["-q"]` di default in `pyproject.toml`. ### Cosa è coperto | File | Copre | | --- | --- | | `test_ansi.py` | stripper ANSI + `visible_len` | | `test_telnet.py` | IACParser (state machine completa) + TelnetConnection (mock asyncio) | | `test_deadsouls.py` | `parse_exits` + `RollingRoomParser` scenario multi‑chunk | | `test_mapper.py` | `Mapper.visit`, link, layout BFS, orphan placement, render | | `test_logger.py` | EventLogger round‑trip JSONL, MD rendering per ogni `kind`, redazione password, idempotenza `close()`, lone‑ESC safety | ### Cosa *non* è coperto - `cli.py` end‑to‑end (richiede telnet server; il vero test è una smoke run contro il docker locale di Dead Souls). - `mapper._render_vertical_row` (casi degeneri con celle non continue; non aggiunti perché non osservati in pratica). - Re‑entrata del logger in un worker thread (lock difensivo è documentato ma non testato). --- ## Lint ```bash ruff check src tests ruff format src tests ``` Config in `pyproject.toml`: ```toml [tool.ruff] line-length = 100 target-version = "py310" ``` (Aggiungi regole strict `select = ["E", "F", "I", "B", "UP"]` se vuoi enforcement.) --- ## Smoke test end‑to‑end Dead Souls docker su `localhost:6666`: ```bash # 2 secondi, log JSONL + MD LOGJSON=$(mktemp /tmp/mud-json.XXXXXX.jsonl) LOGMD=$(mktemp /tmp/mud-md.XXXXXX.md) timeout 2 python -m viandante --host localhost --port 6666 \ --log-json "$LOGJSON" --log-md "$LOGMD" /dev/null 2>&1 || true wc -l "$LOGJSON" "$LOGMD" head -3 "$LOGJSON" head -8 "$LOGMD" ``` Aspettative: almeno 2‑3 righe JSONL (session_start, text, session_end) e un header MD con `host/port/user`. --- ## Come aggiungere un modulo Esempio: nuovo modulo `directions.py` che assorbe `EXIT_ALIASES` da `deadsouls.py`. Procedura: 1. Crea `src/viandante/directions.py` con le costanti e `normalize_exits()`. 2. Aggiorna `deadsouls.py` → re‑import da `directions` per back‑compat. 3. Aggiorna `gmcp.py`, `mapper.py`, `cli.py` → import da `directions`. 4. Scrivi `tests/test_directions.py`. 5. Aggiorna questo file (`docs/development.md`) e `docs/modules.md`. 6. `pytest` + `ruff check`. ### Principi - **Foglia pura sopra orchestratore.** Solo `cli.py` può dipendere da più moduli. Mai il contrario. - **No mutazione cross‑module.** Ogni modulo possiede il suo stato. - **Mai dipendere dal payload GMCP da mapper o deadsouls.** Sono moduli di testo puro. - **Mai import `cli` da moduli bassi.** Solo `cli` sa di `prompt_toolkit`. --- ## Debugging tipico | Sintomo | Tool | | --- | --- | | IAC parser perso un byte | `pytest tests/test_telnet.py -v` + aggiungere print in `_byte()` | | GMCP module non riconosciuto | leggere `gmcp.parse_gmcp` con `data=b"..."` in REPL | | Stanze non rilevate senza GMCP | stampare `state.parser._buf` dopo feed_text | | Mapper collassa in `[mappa troppo larga]` | abbassare `max_cols`/`max_rows` in `render(...)` | | Stanza GMCP arriva ma render non aggiorna | `state.mapper.current_room` punta ancora al vecchio? | | Password finisce nel log | grep `--log-md` per `hunter2`/`***` ratio | | Sessione hung dopo quit | `conn.close()` chiamato? `finally` cleanup? | --- ## Aggiungere un nuovo comando slash 1. In `cli.py:_handle_local_command`, aggiungi un ramo `elif cmd == "foo":`. 2. Se modifica stato → metti la logica prima del `if state.logger is not None: state.logger.local_cmd(...)`. 3. Se inoltra al MUD → usa `await conn.send_line("foo")`. 4. Aggiungi voce nella tabella di `docs/usage.md` e `_print_help()`. 5. Test: aggiungi in `tests/test_cli.py` (futuro; ancora non esiste). --- ## Versioning `__version__` in `__init__.py` (`0.1.0` in v0.2). Bump: - **patch** (0.1.x): bug fix, no nuovi flag. - **minor** (0.x.0): nuovi flag, nuove feature senza break della CLI. - **major** (x.0.0): break di `--log-*` schema eventi o rename simboli pubblici dei moduli. --- ## CI/CD — Gitea Actions I workflow vivono in `.gitea/workflows/`: - **`ci.yml`** — ruff + matrix pytest su Python 3.10/3.11/3.12, trigger su `push` e `pull_request` verso `main`. `concurrency.cancel-in-progress` annulla i run sorpassati (utile per push consecutivi su PR). - **`release.yml`** — `gate` (ruff + pytest) → `build` (`python -m build`, sdist + wheel) su tag `v*`. Gli artifact `viandante-*.tar.gz` e `viandante-*.whl` finiscono nella pagina del job, scaricabili. **Stato operativit\u00e0**: l'infrastruttura YAML \u00e8 committata e pushata (v0.2.0 \u2192 `b20f3c0f`). Finch\u00e9 non registri un **runner** sul Gitea, i job restano in `queued` e il badge `[![ci](...)](...)` rimane grigio. Senza runner la pipeline non gira. ### Setup del runner (una tantum) Sulla macchina che eseguir\u00e0 i job serve **Docker** (l'immagine `ubuntu-22.04` parte in un container): 1. Genera il token di registrazione dalla UI: `http://192.168.0.183:8085/-/admin/runners \u2192 "Crea nuovo runner"` (NON \u00e8 il token account, \u00e8 un one-shot scelto da te). 2. Al primo avvio usa `--rm -it` per vedere in chiaro eventuali errori di registrazione (token sbagliato, permessi su `/var/run/docker.sock`): ```bash docker run --rm -it --name act_runner \ -e GITEA_INSTANCE_URL=http://192.168.0.183:8085 \ -e GITEA_RUNNER_REGISTRATION_TOKEN= \ -e GITEA_RUNNER_NAME=homelab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ gitea/act_runner:latest ``` 3. Quando vedi `Runner is now online` nel log, sostituisci con la versione daemonizzata (`-d`, `--restart unless-stopped`). Un `act_runner` Docker (`gitea/act_runner:latest`) di default pubblica le label `ubuntu-latest`, `ubuntu-24.04`, `ubuntu-22.04`; i workflow di `viandante` usano `runs-on: ubuntu-22.04` (in `.gitea/workflows/*.yml`) che le matcha — ogni job gira in un container pulito basato su Ubuntu 22.04 (Docker‑in‑Docker). > NB sul token: l'URL `/-/admin/runners` genera un **runner globale** > (visibile a tutti i repo del Gitea). Per limitare il runner a un solo > repo, il token si legge da `/-/settings/actions/runners` di quel repo > (la riga registrata appare nella sezione "Runners" del repo specifico, > non nella pagina admin globale). ### Egress verso github.com (possibile blocker) Senza egress dal runner host a `github.com`, le action composite (`actions/checkout@v4`, `actions/setup-python@v5`, `actions/upload-artifact@v4`) non possono essere scaricate e il primo step fallisce con `failed to download action`. Workaround: - **Ridireziona il download delle action** in `app.ini` del server ([chiave `DEFAULT_ACTIONS_URL`](https://docs.gitea.com/administration/config-cheat-sheet#actions-actions), introdotta in Gitea 1.21+, default `https://github.com`): ```ini [actions] DEFAULT_ACTIONS_URL = https:// ``` NB: nomi pi\u00f9 vecchi tipo `ENABLE_GITHUB_ACTIONS_MIRROR` / `ACTIONS_DEFAULT_BASE_URL` **non esistono in 1.26** \u2014 usa solo `DEFAULT_ACTIONS_URL`. - Oppure bundle locale: pre-popolare le action in un image di runner. ### Note pratiche - **Cache `pip`**: `cache: pip` + `cache-dependency-path: pyproject.toml`. Cache key = `python-version + hash(pyproject.toml)`, quindi 3.10/3.11/3.12 hanno cache separate (no cross-version poisoning). - **Tag release pattern**: trigger su `v*` (`v0.2.0`, `v0.3`, `v1.0.0-rc1`). Workflow di release: `git tag vX.Y.Z && git push origin vX.Y.Z`. - **Badge `[![version](https://img.shields.io/...)](...)`** in README.md \u00e8 hardcoded a `v0.2.0`: aggiornalo a mano a ogni release (vedi checklist in `docs/gitea.md`).