diff --git a/src/vouch/jsonl_server.py b/src/vouch/jsonl_server.py index bdd4fd9f..3a04d11b 100644 --- a/src/vouch/jsonl_server.py +++ b/src/vouch/jsonl_server.py @@ -1104,7 +1104,12 @@ def handle_request(envelope: dict) -> dict: "id": req_id, "ok": False, "error": {"code": "dead_claim_refs", "message": str(e)}, } - except (ValueError, ProposalError, ArtifactNotFoundError) as e: + # lifecycle validation failures (unknown goal status, self-supersede / + # -contradict) are caller errors, not server faults — the sibling of + # ProposalError. map to invalid_request so jsonl/http match the mcp contract + # (kb_set_goal_status catches LifecycleError and re-raises it as ValueError); + # otherwise a bad request reads as a retryable internal_error. + except (ValueError, ProposalError, ArtifactNotFoundError, life.LifecycleError) as e: return { "id": req_id, "ok": False, "error": {"code": "invalid_request", "message": str(e)}, diff --git a/tests/test_jsonl_server.py b/tests/test_jsonl_server.py index 2cd8d028..7554e93c 100644 --- a/tests/test_jsonl_server.py +++ b/tests/test_jsonl_server.py @@ -381,3 +381,22 @@ def test_import_check_bundle_path_fenced_on_remote(store: KBStore, monkeypatch) "params": {"bundle_path": "/etc/passwd"}}) assert not resp["ok"] assert "project root" in resp["error"]["message"] + + +def test_jsonl_set_goal_status_bad_status_is_invalid_request(store: KBStore, monkeypatch) -> None: + # a typo'd goal status is a caller error, not a server fault. set_goal_status + # validates the status before the goal lookup and raises LifecycleError (a + # RuntimeError, not a ValueError); the dispatcher must map it to + # invalid_request like mcp does, or a client sees internal_error and wrongly + # treats a bad request as retryable. (status is checked before goal lookup, + # so no goal need exist.) + monkeypatch.chdir(store.root) + resp = handle_request( + { + "id": "1", + "method": "kb.set_goal_status", + "params": {"goal_id": "g-missing", "status": "bogus"}, + } + ) + assert not resp["ok"] + assert resp["error"]["code"] == "invalid_request"