Adding a feature
A feature is done when it works end to end and every layer has its proof. The order is fixed:
- Engine API.
- Automation tool with an end-to-end test.
- UI.
- Parity entry with evidence.
- Documentation page update.
crates/engine, and nothing below L7 may depend on egui.1. Engine API
- Put the behaviour in the engine, or in the crate below it that owns the concern (
http,grpc,ws,interop,script,store, and so on). - Return errors through the crate's error type and
?. - The UI and the automation tool must share this one implementation.
2. Automation tool
Every user-facing feature must be callable without the GUI. Tools live in crates/automation/src/tools.rs.
- Add a
ToolDefto the table returned bytools(). It hasname(snake_case),title,description,input_schemaand theread_onlyanddestructiveflags. Declare required and optional arguments with thereq(...)andopt(...)helpers in theschema!macro. - Add the matching arm to
dispatch(...). Unknown tools and unknown arguments are refused before any state changes. - Errors must say what was wrong and what is allowed.
- Send every path argument through the session, so
--rootconfines it. A path outside the root gets the same refusal whether or not it exists, including paths reached by nested dispatch. - Write atomically. Never discard unsaved work silently. Cap anything echoed to an agent (bodies are capped at 1 MiB).
- Add an end-to-end test in
crates/automation/tests/(seetools.rsandengine_tools.rsthere). The CLI side is exercised inapps/postguy-cli/tests/. - Check it yourself with
postguy-cli:
cargo run -p postguy-cli -- tools
cargo run -p postguy-cli -- run --script steps.json --root DIR
3. UI
Implement the interface in crates/ui-egui. These rules apply to every UI change:
- Never add a repaint loop.
- Never run I/O, parsing, formatting, searching or scripts on the UI thread.
- Use Lucide icons from
assets/icons/and Poppins only (see Assets and clean-room). - Check undo/redo and dirty tracking, save fidelity and session restore.
- Look at the result:
- Launch
postguy --control FILE. Usepostguy-cli ui --control FILE screenshot --out x.png(plusinspect,commandandset). - Or take a headless shot with
cargo run -p postguy-ui-egui --example shot.
The control channel refusesclick,keyandtypetoday. See UI control channel and Known gaps. - Launch
4. Parity entry
Every user-facing feature has a [[feature]] entry in parity/postman-features.toml.
| Field | Meaning |
|---|---|
id, title, area, tier | Identity. Areas are A collections, B environments-and-variables, C http, D grpc, E websocket, F automation, G imports-and-exports, H ui-shell, I platform-packaging. Tiers are P0 to P3 and N/A. |
status | planned, partial, shipped or na. |
notes | What is missing. Mandatory honesty for partial. A reason for na. |
commands | Registry ids that must exist in crates/engine/src. |
tools | Tool names that must exist in crates/automation/src. |
evidence | Passing tests as path/to/file.rs::test_name. |
shippedrequires cited tests whose paths and names exist.plannedandnaentries must not cite evidence.
Run the check:
cargo xtask parity # validate and report
cargo xtask parity --partial # list partial entries
"Shipped" means it exists and is tested, not that it is "as good as another tool". Say in notes what is still missing. Also make sure no other entry just became untrue.
The same facts show to users on Feature status.
5. Documentation
Update the matching page under docs/content/ in the same pull request as any user-facing change. See Writing the documentation.
Testing
Review every change at four zooms. Do it while you make the change, not at the end.
| Zoom | Check |
|---|---|
| Clause | Checked arithmetic, get() instead of [i], char-boundary slicing, the right error through ?, < against <=, 0-based against 1-based seams, capped allocations. |
| Function | Is it total? Name an input that panics. Walk empty, zero, max, malformed and cyclic inputs. Is the return type honest? |
| File or module | Two sources of truth, dead or duplicated code, smallest public surface, doc comments still true. |
| Project | Layering, undo/redo and dirty tracking, save fidelity, session restore, --root confinement, a matching tool, a parity note that just became untrue. |
Tests mirror the zooms: property, unit, crate integration, end to end. Scale the zoom to the change.
Hermetic tests
Tests never touch the internet. Use the loopback servers in crates/testkit (a dev-dependency, never shipped): HTTP, TLS, an OAuth token endpoint, gRPC with our own echo.proto, WebSocket and a proxy.
Fuzzing
cargo xtask fuzz --time 300
fuzz mutates committed seed files under fuzz/seeds/. It runs these targets in child processes through postguy-cli fuzz-target: url, vars, curl, data, openapi, har and script.
- A panic, a fatal signal or a hang is a finding.
- Findings are saved under
fuzz-out/findings/, which is git-ignored.
Optional flags: --time SECONDS, and --seed N for a reproducible run. CI runs it nightly with --time 900.
To turn a finding into a regression test:
- Read the saved input and the
whyin the finding's info file. - Reduce the input to a small synthetic sample. Never commit corpus-derived files.
- Write a test that feeds it to the failing function and asserts a clean
Error a handled result. - Run the test and watch it fail (panic) before the fix.
- Fix, run it again, then commit the fix and the test together.
Never-crash rules
A malformed file, a hostile response, a bad tool argument, a corrupt settings file or a full disk must produce an error the user can act on. It must never crash and never lose work. This outranks feature work.
- Non-test code uses
Result. Nounwrap(),expect(),panic!,unreachable!,todo!orunimplemented!unless provably infallible, with a comment saying why. - No
unsafe. The workspace setsunsafe_code = "forbid". - Treat every input-derived number as hostile. Use
get(), checked or saturating arithmetic, and cap allocations sized by the input. - Bound recursion with seen-sets or depth limits (JSON,
$ref, proto messages, variable references, redirects). - Entry points that run untrusted input (scripts, importers, commands, file import and export) keep a last-resort
catch_unwindguard. Keeppanic = "unwind". Never setpanic = "abort". - A clean crate carries
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]. New crates start with it. - Every crash fix comes with a small synthetic regression test that panicked before the fix.