# Development ## Setup ```bash python3 -m venv venv venv/bin/pip install -r requirements-dev.txt ``` ## Running locally without Okta or a real PPS Use a dev config (never edit the real `config.toml`) with static auth: ```toml [auth] mode = "static" [auth.static] username = "admin" password = "admin" email = "dev@example.invalid" role = "admin" ``` ```bash PPSQ_CONFIG=/tmp/dev-config.toml PPSQ_ALLOW_INSECURE=1 venv/bin/python wsgi.py ``` - `PPSQ_CONFIG` points at your dev file so nothing touches the real config. - `PPSQ_ALLOW_INSECURE=1` allows an `http://` PPS base_url. - Static mode logs a loud warning and refuses non-loopback binds (override with `PPSQ_ALLOW_INSECURE_AUTH=1`). - Point `[pps] base_url` at a mock server, or a throwaway PPS folder. ## Tests ```bash venv/bin/python -m pytest # ~57 tests, < 1s ``` `tests/conftest.py` guarantees isolation: a session-scoped autouse fixture sets `PPSQ_CONFIG` to a temp copy of `config.example.toml` before any app import, and `ConfigStore` refuses to open the default `config.toml` under pytest. No test starts a worker thread — they call `queue._process(job)` synchronously with a `FakePPS` that records every call. Suites: | File | Covers | |------|--------| | `test_config_store.py` | round-trip (comments preserved), atomic write/perms/backup, redaction, validation, restart-vs-live, client rebuild, migration, isolation guard | | `test_auth.py` | `is_allowed`/`safe_next` (pure), static login, CSRF, admin gate | | `test_oidc.py` | OIDC callback allow/deny/role/demotion (monkeypatched token) | | `test_report_release.py` | pipeline: no-`deletedfolder` rule, lazy refind, fallback, destinations | | `test_paging.py` | cursor from raw batch, `has_more` boundary, `_clamp_limit` | | `test_prefs.py` | override/default/fallback/isolation | | `test_jobs_visibility.py` | user attribution, `targetfolder` validation, per-user scoping | ## Project layout See `AGENTS.md` for the module map and the invariants you must not break. In short: - `wsgi.py` constructs everything; `app.py` is a factory with no import-time side effects. - Config flows through immutable `ConfigStore` snapshots (one per request, one per job). - Report & Release is data-driven in `pipeline.py` — add a step there, not in `worker.py`. - Frontend is vanilla JS (`static/app.js`, `static/admin.js`); no build step. ## Adding things - **A pipeline step:** `@step("name")` in `pipeline.py` + add to `ALLOWED_STEPS`. - **A config key:** `config.example.toml` (with a comment) → read from snapshot → if admin-editable, add to `ADMIN_EDITABLE` + validation in `config_store.py`. - **A preference:** a `PrefSpec` in `prefs.py` `PREF_SPECS`. Match surrounding style: `from __future__ import annotations`, `pps.*` logger names, `%`-style logging, `_`-prefixed privates.