# mini-serve — Development Plan
Coverage target: **≥ 80%** line coverage. The connection-lifecycle fixes need real
socket-level tests (raw TCP, actual timeouts), not just unit tests of pure functions —
budget for that when estimating phase 0.3.0.
## Phase 0.1.0 — MVP: routing + single-traversal dispatch
*Exit criteria: register routes, dispatch by method and path param, one trie walk per
request from the start (not retrofitted later).*
1. **Commit: `App<S>`, `Node`/trie, single-traversal `find()`.** Primary test: register
`GET /items/:id`, request `/items/42`, assert the extracted param equals `"42"` and
the handler runs exactly once (instrument a call counter — proving one traversal, not
the historical three).
2. **Commit: method dispatch + 405 with `Allow`.** Primary test: register only `GET` on
a path, send `POST`, assert `405` and an `Allow: GET, HEAD` header with the exact
expected value — not just status-code presence.
3. **Commit: HEAD-via-GET sharing the same response headers.** Primary test: register a
`GET` handler returning a known `Content-Length`, send `HEAD`, assert the response
has the *identical* `Content-Length` and an empty body — the regression test for the
zeroed-length bug.
4. **Commit: percent-decoding + `+`-as-space in routing/query parsing.** Primary test:
request `/api/%69tems` against a route registered as `/api/items`, assert it matches;
separately, parse a query string `q=hello+world`, assert the decoded value is
`"hello world"`.
## Phase 0.2.0 — State sharing + typed extraction
*Exit criteria: state sharing is a refcount bump; path params extract via serde directly.*
5. **Commit: `State::from_arc`, drop the deep-clone-and-rewrap.** Primary test: hold a
`Weak` reference to the original `Arc<S>`, make several requests, assert the strong
count only ever reflects in-flight requests — proving no extra `Arc` was allocated
per request.
6. **Commit: `MapDeserializer`-based `path_params<T>()`.** Primary test: extract a typed
struct with a `u64` field from path params, assert the parsed value equals the
expected number — not `is_ok()` — and that an unparseable segment returns a `400`
with the documented message.
## Phase 0.3.0 — Connection lifecycle hardening
*Exit criteria: TLS handshake timeout, bounded accept-error backoff, loopback-only
ephemeral binds, and shutdown that isn't blocked by a saturated semaphore — each with a
test that exercises the actual failure mode, not just the code path.*
7. **Commit: TLS handshake timeout.** Primary test: open a raw TCP connection to a TLS
listener and send nothing; assert the connection is closed within the configured
timeout and the accept loop continues serving other clients.
8. **Commit: bounded accept-error backoff.** Primary test: inject a simulated repeated
accept error (via a test-only listener wrapper) and assert the loop's CPU-bound retry
rate is capped — e.g. assert at least N milliseconds elapse between M consecutive
error iterations, rather than a busy spin.
9. **Commit: loopback-only ephemeral binds.** Primary test: call `bind_ephemeral()`,
assert the bound address's IP is exactly `127.0.0.1`.
10. **Commit: shutdown-fair permit acquisition.** Primary test (the regression test for
the hang): saturate `max_connections = 1` with one long-lived in-flight request,
trigger shutdown, assert the shutdown future completes once that one request
finishes — proving shutdown isn't starved by the semaphore.
## Phase 0.4.0 — CORS + error-response hardening
*Exit criteria: the credentialed-wildcard bypass is unrepresentable; 5xx bodies never
leak internals.*
11. **Commit: `CorsConfigBuilder::build() -> Result<...>`.** Primary test: attempt to
build a config with `allow_all_origins: true` and `credentials: true`, assert it
returns `Err(CorsConfigError::CredentialedWildcard)` in *both* debug and release
profiles (run the test under both, or assert on a profile-independent code path).
12. **Commit: default error handler sanitizes 5xx, passes through 4xx.** Primary test:
a handler returns an `Error::internal(..., "raw db connection string leaked")`;
assert the client-visible body is the generic message while a test-only log sink
captures the real one. Separate test: a `bad_request` message passes through intact.
13. **Commit: CORS preflight only for real routes.** Primary test: `OPTIONS` with an
`Origin` header against an unregistered path returns `404`, not `204`.
## Phase 0.5.0 — Routing performance
*Exit criteria: static-segment matching doesn't clone the param map at every node it
merely passes through.*
14. **Commit: borrow-through param passing with truncate-on-backtrack.** Primary test:
a benchmark or allocation counter shows zero `PathParams` clones for a request that
matches a route with no dynamic segments, versus at least one per static ancestor
node before the fix.
## Phase 0.6.0 — Publish
15. **Commit: `err`/`log`/`tls` feature docs + doc comments on every public item.**
16. **Commit: CI** — `cargo test`, `cargo test --all-features`, `cargo clippy -- -D warnings`,
`cargo llvm-cov --fail-under-lines 80`.
17. **Publish `0.6.0`** to crates.io (name confirmed available).