change_status cannot say why an application ended, and the API description the tests check against has drifted #16

Closed
opened 2026-10-01 10:48:05 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo-mcp at 0b29e70b9, comparing tests/openapi.json with the description generated from postulo 95348908d.

The core's POST /applications/{pk}/status takes an end_reason with rejected, withdrawn or ghosted since #239, and the insights and the job-search report read it. change_status sends only status and note, so an ending recorded through an agent never carries a reason. The tests did not notice because the API description they are held to is a snapshot from before that change, and nothing compares it with the core.

  • Where: src/postulo_mcp/server.py:275-283 (change_status); tests/fake_api.py:17-25 (the refresh recipe and "The snapshot in the tree was taken at postulo 5ee31c66a"); tests/openapi.json
  • What happens:
    • A person tells the agent "they turned me down, the salary was too low". The agent calls change_status(status="rejected", note=...); the ending lands on the timeline with no reason, so applications/endings.py (read by applications/analytics.py and applications/reports.py in the core) counts it as an ending without a reason. A reason learnt later, which the core accepts as end_reason sent with the status the application already has (api/schemas.py:494-501), cannot be added through the agent either.
    • Generating the description from the core snapshot and comparing: the checked-in file has 32 paths against 38. StatusIn lacks end_reason; the description lacks GET /offers, GET /offers/{pk}, POST /listings/{pk}/events, PATCH and DELETE /reminders/{pk}, GET /listings/choices and PUT /captures/{pk}/rendering; the list calls the MCP uses gained after_id and updated_since; and the refusal responses are not problem documents. No operation the MCP calls has been removed, so nothing fails today, which is the point: the fake can no longer tell this repository that the API grew.
  • Why it matters: the fake was built from the description (#3) precisely so tools could not drift from the API unnoticed. A snapshot that is never refreshed brings that back slowly, and here it has already hidden a field the core added for exactly this kind of client.
  • Fix: add end_reason: str = "" to change_status, with the six values (pay, location, not_a_match, filled_internally, my_choice, other) and "only with rejected, withdrawn or ghosted" in its description, and send it only when set. Refresh tests/openapi.json with the recipe in tests/fake_api.py. Add a scheduled CI job, or a step on a tag, that regenerates the description from the core's main and fails when an operation or request schema this server calls differs from the snapshot.

Done when

  • tests/test_server.py has a test that calls change_status(application_id=7, status="rejected", end_reason="pay") and asserts the payload carries end_reason; against the refreshed tests/openapi.json the fake accepts it, and against today's snapshot it would refuse it.
Found on 2026-09-29 in a read-only review of postulo-mcp at 0b29e70b9, comparing `tests/openapi.json` with the description generated from postulo 95348908d. The core's `POST /applications/{pk}/status` takes an `end_reason` with `rejected`, `withdrawn` or `ghosted` since #239, and the insights and the job-search report read it. `change_status` sends only `status` and `note`, so an ending recorded through an agent never carries a reason. The tests did not notice because the API description they are held to is a snapshot from before that change, and nothing compares it with the core. - **Where:** `src/postulo_mcp/server.py:275-283` (`change_status`); `tests/fake_api.py:17-25` (the refresh recipe and "The snapshot in the tree was taken at postulo 5ee31c66a"); `tests/openapi.json` - **What happens:** - A person tells the agent "they turned me down, the salary was too low". The agent calls `change_status(status="rejected", note=...)`; the ending lands on the timeline with no reason, so `applications/endings.py` (read by `applications/analytics.py` and `applications/reports.py` in the core) counts it as an ending without a reason. A reason learnt later, which the core accepts as `end_reason` sent with the status the application already has (`api/schemas.py:494-501`), cannot be added through the agent either. - Generating the description from the core snapshot and comparing: the checked-in file has 32 paths against 38. `StatusIn` lacks `end_reason`; the description lacks `GET /offers`, `GET /offers/{pk}`, `POST /listings/{pk}/events`, `PATCH` and `DELETE /reminders/{pk}`, `GET /listings/choices` and `PUT /captures/{pk}/rendering`; the list calls the MCP uses gained `after_id` and `updated_since`; and the refusal responses are not problem documents. No operation the MCP calls has been removed, so nothing fails today, which is the point: the fake can no longer tell this repository that the API grew. - **Why it matters:** the fake was built from the description (#3) precisely so tools could not drift from the API unnoticed. A snapshot that is never refreshed brings that back slowly, and here it has already hidden a field the core added for exactly this kind of client. - **Fix:** add `end_reason: str = ""` to `change_status`, with the six values (`pay`, `location`, `not_a_match`, `filled_internally`, `my_choice`, `other`) and "only with rejected, withdrawn or ghosted" in its description, and send it only when set. Refresh `tests/openapi.json` with the recipe in `tests/fake_api.py`. Add a scheduled CI job, or a step on a tag, that regenerates the description from the core's main and fails when an operation or request schema this server calls differs from the snapshot. ## Done when - `tests/test_server.py` has a test that calls `change_status(application_id=7, status="rejected", end_reason="pay")` and asserts the payload carries `end_reason`; against the refreshed `tests/openapi.json` the fake accepts it, and against today's snapshot it would refuse it.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:48:05 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo-mcp#16
No description provided.