wrongmove/docs/plans/2026-02-22-debug-cli-design.md

107 lines
4.3 KiB
Markdown
Raw Normal View History

2026-02-22 15:05:18 +00:00
# Debug CLI Design
**Date:** 2026-02-22
**Status:** Approved
## Goal
Add a debug CLI that mirrors all web UI interactions, with superuser access to impersonate any user. Purpose: debug flows without authorization overhead.
## Decisions
- **Scope:** All API endpoints (listings, decisions, POIs, tasks, districts)
- **Execution mode:** Switchable — direct service calls (default) or HTTP to running API (`--http`)
- **User identity:** `--user-email` flag to impersonate any user
- **Location:** New `cli/` package with submodules, registered under a `debug` group in `main.py`
- **Output:** Pretty-printed by default, `--json` flag for machine-readable output
## Structure
```
cli/
__init__.py # Exports all groups for registration in main.py
_context.py # Shared context: user identity, HTTP vs direct, output formatting
listings.py # list, detail, stream, refresh
decisions.py # set, list, remove
pois.py # create, list, update, delete, calculate, distances
tasks.py # status, list, cancel, clear
districts.py # list
```
### Usage
```bash
python main.py debug --user-email user@example.com listings list --type RENT --min-bedrooms 2
python main.py debug --user-email user@example.com --http decisions set 12345 liked --type RENT
python main.py debug --user-email user@example.com --json pois list
```
## Shared Context (`_context.py`)
- **`CliContext` dataclass:** Holds `user_email`, `use_http`, `json_output`, `api_base_url`
- **`get_user()`:** Creates `User(sub=email, email=email, name=email)` from provided email — no token validation
- **Direct mode:** Instantiates repositories and calls service functions, passing the fake `User`
- **HTTP mode:** Mints a self-signed JWT using `JWT_SECRET` from env (passkey-style token), makes requests via `httpx`
- **Output helpers:** `output(data, json_mode)` — prints JSON or pretty-formatted text
## Command Mapping
### `listings` group
| Command | Direct mode | HTTP mode |
|---------|------------|-----------|
| `list` | `listing_service.get_listings()` | `GET /api/listing` |
| `detail <id>` | Repository + poi_service | `GET /api/listing/{id}/detail` |
| `stream` | `export_service.export_to_geojson()` | `GET /api/listing_geojson/stream` |
| `refresh` | `listing_service.refresh_listings()` | `POST /api/refresh_listings` |
### `decisions` group
| Command | Direct mode | HTTP mode |
|---------|------------|-----------|
| `set <id> <liked\|disliked>` | `decision_service.set_decision()` | `PUT /api/decisions/{id}` |
| `list` | `decision_service.get_user_decisions()` | `GET /api/decisions` |
| `remove <id>` | `decision_service.remove_decision()` | `DELETE /api/decisions/{id}` |
### `pois` group
| Command | Direct mode | HTTP mode |
|---------|------------|-----------|
| `list` | `poi_service.get_user_pois()` | `GET /api/poi` |
| `create` | `poi_service.create_poi()` | `POST /api/poi` |
| `update <id>` | `poi_service.update_poi()` | `PUT /api/poi/{id}` |
| `delete <id>` | `poi_service.delete_poi()` | `DELETE /api/poi/{id}` |
| `calculate <id>` | `poi_service.trigger_calculation()` | `POST /api/poi/{id}/calculate` |
| `distances` | `poi_service.get_distances_for_listing()` | `GET /api/poi/distances` |
### `tasks` group
| Command | Direct mode | HTTP mode |
|---------|------------|-----------|
| `status <id>` | `task_service.get_task_status()` | `GET /api/task_status` |
| `list` | `task_service.get_user_tasks()` | `GET /api/tasks_for_user` |
| `cancel <id>` | `task_service.cancel_task()` | `POST /api/cancel_task` |
| `clear` | `task_service.clear_all_tasks()` | `POST /api/clear_all_tasks` |
### `districts` group
| Command | Direct mode | HTTP mode |
|---------|------------|-----------|
| `list` | `district_service.get_all_districts()` | `GET /api/get_districts` |
## Error Handling
- **Direct mode:** Catch service exceptions, print readable messages (or `{"error": "..."}` in JSON mode)
- **HTTP mode:** Print status code + response body on non-2xx
- **Missing services:** Fail fast with clear message about which service (DB, Redis) is unavailable
## Testing
- Unit tests for `_context.py` (JWT minting, user creation, output formatting)
- Integration tests for representative commands in direct mode (mocked repositories)
- No HTTP-mode tests (that's API testing, already covered)
## Dependencies
No new dependencies. Uses existing: `click`, `httpx`, `pyjwt`.