feat(baseline-kit): JSON/API response snapshot helper (api-snapshot modality) - #2207
Conversation
…odality) Add baseline_kit/snapshot.py to unblock snapshotting nested JSON API responses (e.g. FastAPI GET /managers) against pytest-regressions' data_regression fixture -- no numpy/pandas, unlike golden.check_metrics. Public API: * normalize_response(payload, *, exclude=(), sort_key=None) -- stable copy * check_snapshot(data_regression, payload, *, exclude, sort_key, basename) * response_to_payload(response) -- duck-typed TestClient/httpx adapter Normalization redacts volatile fields (bare keys at any depth, anchored dotted paths, and `*` wildcard segments), optionally reorders record lists via sort_key, and recursively sorts dict keys for deterministic output. Exports the new names from __init__, adds tests/test_snapshot.py (14 tests, incl. a data_regression round-trip golden), documents the modality + exclude path syntax in README, and bumps the package to 0.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workflow source neededPR #2207 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely. Please do one of:
Once a valid source is present, this warning will not be reposted. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b35367123c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| anchored, bare = _match_paths(exclude) | ||
| redacted = _redact(payload, anchored, bare) | ||
| if sort_key: | ||
| prepared = [(_split(path), fn) for path, fn in sort_key.items()] |
There was a problem hiding this comment.
Support sorting root list payloads
For APIs that return a bare top-level array, e.g. GET /managers -> [...], there is currently no string path that can register a sort key for the payload itself: _split() always returns at least one segment, while _apply_sort_keys() only sorts the current list when the path tail is empty. That leaves order-unstable top-level lists producing flaky snapshots even though normalize_response otherwise accepts top-level list payloads. Consider reserving an empty/root path and converting it to [] before applying sort keys.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Adds an api-snapshot testing modality to app-baseline-kit so nested JSON/API responses can be golden-mastered via pytest-regressions' data_regression fixture (no numpy/pandas), complementing the existing flat-metric golden.check_metrics. Provides redaction (bare-key / dotted / wildcard paths), opt-in list reordering via sort_key, recursive key sort, and a duck-typed adapter for TestClient/httpx responses. Bumps the package to 0.2.0 and documents the new modality in the README.
Changes:
- New
baseline_kit/snapshot.pyexposingnormalize_response,check_snapshot,response_to_payload, exported from__init__. - Tests covering bare/dotted/wildcard redaction, key sorting, list ordering,
sort_key, tuple coercion, no-mutation, top-level lists, scalar pass-through, duck-typed adapter, and adata_regressionround-trip with golden YAML. - README adds an "api-snapshot modality" section and exclude-syntax table;
pyproject.tomlbumped 0.1.0 → 0.2.0.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/app-baseline-kit/baseline_kit/snapshot.py | New normalization + snapshot helpers and adapter. |
| packages/app-baseline-kit/baseline_kit/init.py | Re-exports the new public API. |
| packages/app-baseline-kit/tests/test_snapshot.py | Unit tests for redaction, sorting, adapter, round-trip. |
| packages/app-baseline-kit/tests/test_snapshot/test_check_snapshot_round_trip.yml | Golden snapshot for round-trip test. |
| packages/app-baseline-kit/README.md | Documents the new modality + exclude syntax. |
| packages/app-baseline-kit/pyproject.toml | Version bump 0.1.0 → 0.2.0. |
| here = [fn for p, fn in sort_keys if not p] | ||
| descended = [] | ||
| for index, value in enumerate(node): | ||
| token = str(index) | ||
| child = [(p[1:], fn) for p, fn in sort_keys if p and _seg_matches(p[0], token)] | ||
| descended.append(_apply_sort_keys(value, child)) | ||
| if here: | ||
| descended = sorted(descended, key=here[0]) |
| if isinstance(node, (list, tuple)): | ||
| out_list: list[JSONValue] = [] | ||
| for index, value in enumerate(node): | ||
| token = str(index) | ||
| child_anchored = [] | ||
| dropped = False | ||
| for path in anchored: | ||
| if _seg_matches(path[0], token): | ||
| if len(path) == 1: | ||
| dropped = True | ||
| break | ||
| child_anchored.append(path[1:]) | ||
| if dropped: | ||
| continue | ||
| out_list.append(_redact(value, child_anchored, bare)) | ||
| return out_list |
| def check_snapshot( | ||
| data_regression, | ||
| payload: JSONValue, | ||
| *, | ||
| exclude: Iterable[str] = (), | ||
| sort_key: SortKeys | None = None, | ||
| basename: str | None = None, | ||
| ) -> None: |
Automated Status SummaryHead SHA: 09dc85a
Coverage Overview
Coverage Trend
Top Coverage Hotspots (lowest coverage)
Updated automatically; will refresh on subsequent CI/Docker completions. Keepalive checklistScopeNo scope information available Tasks
Acceptance criteria
|
🤖 Bot Comment Handler
The agent has been assigned to this PR to address the bot review comments. Instructions for agent
The bot comment handler workflow has prepared context in the artifacts. |
What
Adds an API/JSON response-snapshot helper to
packages/app-baseline-kit/, unblocking the api-snapshot testing modality. The next consumer is Manager-Database (a FastAPI app), which will snapshotGET /managers-style JSON responses against a seeded DB.golden.check_metricsonly handles a flatdict[str, float|int]via thenum_regressionfixture (numpy/pandas). API responses are nested JSON, so this uses pytest-regressions'data_regressionfixture instead — arbitrary YAML-able data, no numpy/pandas — keeping it cheap for lightweight services.New public API (
baseline_kit/snapshot.py, exported from__init__)normalize_response(payload, *, exclude=(), sort_key=None)— return a snapshot-stable copy of a JSON-able payload (redaction + deterministic ordering); call directly to inspect/assert structure.check_snapshot(data_regression, payload, *, exclude=(), sort_key=None, basename=None)— normalize thendata_regression.check(...); re-bless with--force-regen.response_to_payload(response)— duck-typed adapter for any object with.json()+.status_code(Starlette/FastAPITestClient,httpx.Response) →{"status_code": ..., "json": <body>}. No fastapi/httpx dependency.Normalization (order: redact → sort_key reorder → recursive key sort)
Input never mutated;
tuplecoerced tolist.excludepath syntax (matched nodes are dropped):.)"id","created_at""meta.request_id"*"items.*.updated_at"updated_atin every element of top-levelitems*"data.*"data*consumes exactly one level (list index or dict key).List ordering: preserved by default; pass
sort_key={<path-to-list>: <key-fn>}to sort an order-unstable record list before snapshotting (or pre-sort in the adapter).Design decisions to confirm
"id"/"created_at"redacts that field wherever it nests (the common timestamp/autoincrement case); a dotted path like"meta.request_id"only removes that exact root-anchored location. Flagging in case you'd prefer all paths anchored.sort_keyis given. Kept simple/correct; sort is opt-in per-path rather than guessing a key.Files
baseline_kit/snapshot.py,tests/test_snapshot.py,tests/test_snapshot/test_check_snapshot_round_trip.yml(golden)baseline_kit/__init__.py(exports),pyproject.toml(0.1.0 → 0.2.0),README.md(api-snapshot section + exclude syntax)Tests / lint
cd packages/app-baseline-kit && pip install -e . pytest-regressions && pytest -q→ 14 passed (incl. adata_regressionround-trip; covers bare/dotted/wildcard redaction, key sorting, list handling, sort_key reorder, no-mutation, top-level list, duck-typed adapter).ruff check+ruff format --checkclean (root config: line-length 100, py312).No new heavy deps (no numpy/pandas/fastapi/httpx).
🤖 Generated with Claude Code