property-repro
GitHub用于通过属性测试复现 Honcho API 报告的错误,生成失败用例并修复。适用于处理5xx错误、状态码异常及数据往返问题,利用Schemathesis等工具进行模糊测试和自动化验证。
Trigger Scenarios
Install
npx skills add plastic-labs/honcho --skill property-repro -g -y
SKILL.md
Frontmatter
{
"name": "property-repro",
"description": "Reproduce a reported Honcho API bug (a 5xx, a wrong status code, data that doesn't round-trip) as a failing Hypothesis\/Schemathesis test, then fix it. Use when an issue reports an unexpected API error, when hunting for 5xx bugs in a route, or before changing request validation or error handling."
}
Reproducing API bugs with property tests
tests/property/ fuzzes the /v3 API in-process against a real Postgres test
database. Every test asserts that no response is a 5xx and that no unhandled
exception escapes (TestClient re-raises them). Start here when an issue says
"I got a 500 when…": the shortest path is usually to get one of these tests to
fail on the reported input, then fix the cause.
The harnesses
| File | What it drives | Reach for it when |
|---|---|---|
test_api_no_server_errors.py |
Schemathesis: every operation, from the app's own OpenAPI schema, in positive and negative mode. Path and body IDs point at seeded resources (seeded_ids) most of the time. |
The report is one request. |
test_api_inputs.py |
Hand-written strategies for what the schema doesn't describe: multipart uploads, the filter DSL, odd JSON in metadata/configuration, store-then-read round trips. | The input is a file, a filter, or a JSON value. |
test_api_stateful.py |
A RuleBasedStateMachine over workspaces, peers, sessions, messages, conclusions, including use after delete. |
The bug needs a history ("after I deleted the session…"). |
test_api_auth.py |
The per-operation fuzzer with auth on and a random token per request (none, forged, expired, admin, workspace/peer/session/named-scope keys across two workspaces), checked against an oracle built from the route policy. | The report involves a scoped key, a 401/403, or data visible to the wrong caller. |
test_api_concurrency.py |
Batches of requests on colliding names fired at once over httpx's ASGI transport, each on its own DB connection; also checks get-or-create leaves exactly one row. | The bug is intermittent, a spurious 409, or only shows up under load. |
They are skipped unless pytest gets --fuzz (CI passes it; the pre-push hook
doesn't, to keep it fast), so every command below includes it. All of them
use fuzz_client (one DB session per request, as in production) from
tests/property/conftest.py.
Workflow
-
Try the broad fuzzer on the route first. Select by operation label and raise the example count:
HONCHO_FUZZ_EXAMPLES=1000 uv run pytest tests/property/test_api_no_server_errors.py \ --fuzz -k "messages and POST and not list and not upload" -n 0 -p no:cacheprovider \ --tb=line --show-capture=noA failure prints the exception type and a curl command for the shrunk request.
-
If it passes, check that the route is actually being reached. A route that only ever gets 404/422 is only being fuzzed on its error paths:
HONCHO_FUZZ_STATS=/tmp/stats.txt uv run pytest --fuzz tests/property/test_api_no_server_errors.py awk '{k=$1" "$2" "$3; t[k]++; if ($4<300) ok[k]++} END {for (k in t) if (!ok[k]) print k}' /tmp/stats.txtFix coverage before concluding there is no bug: add the ID field to
BODY_ID_FIELDS, seed the resource the route needs, or write a targeted strategy intest_api_inputs.py. -
Pin the reported input. Put the issue's exact input in a test, either as an
@example(...)on a property or as a plain test, and confirm it fails before you touchsrc/. -
Fix the cause, not the status code. Follow the existing conventions:
- NUL (
\x00) is stripped at the schema boundary (NulStripped,strip_nulinsrc/utils/sanitization.py), not rejected. A new free-form text or JSON field needs the same treatment. - A handler that builds a create schema from a path ID (get-or-create of a
parent workspace or session) goes through
schema_from_path(src/utils/path_params.py), which turns a bad ID into a 422 atpath.<param>. AValidationErroranywhere else is a server bug and stays a 500. - Values Postgres can't store are a 422 only for an explicit allowlist of
SQLSTATEs (
_is_input_data_errorinsrc/main.py). Widen it only for an input class, and match narrowly when the SQLSTATE also covers server bugs (see the 22P02 surrogate case). - Library parse failures on uploads become a
ValidationException, caught around the library call only. - Never add a blanket
except Exceptionto turn a 500 into a 4xx.
- NUL (
-
Leave a deterministic regression test in
tests/routes/(most land intest_validation_api.py) that sends the shrunk input. Hypothesis runs withdatabase=None, so a property failure is not replayed on the next run unless you pin it.
Gotchas
- Background work does not run under test (the deriver, deletion cleanup, the reconciler). Only the "deletion pending" state is reachable; LLM and embedding calls are mocked.
- DB state persists across Hypothesis examples within one test function. That
helps (more collisions) but means a failure can depend on earlier examples;
re-run with
-n 0and a fixed--hypothesis-seedto replay it. - Schemathesis' positive mode respects the OpenAPI schema. When the server
enforces a constraint the schema doesn't declare (e.g.
peer_idmust match the name pattern), positive-mode requests get 422s and never reach the handler. That gap is a schema bug in its own right. - Default example counts are small so the suite stays fast. Raise
HONCHO_FUZZ_EXAMPLES(andHONCHO_FUZZ_STEPSfor the state machine) locally when hunting.
Version History
- d11a764 Current 2026-11-02 23:33


