-
Notifications
You must be signed in to change notification settings - Fork 0
fix(offchain): extract inner result field to match on-chain response shape #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -261,9 +261,19 @@ def _parse(self, response: Any) -> _PollSnapshot: | |
| return _PollSnapshot(status="processing") | ||
| status = str(payload.get("status", "")).lower() | ||
| if status == "ok": | ||
| # Match the on-chain `mech_response` ApiSpec (`response_key: result`): | ||
| # extract the inner `result` field so downstream (DecisionReceive | ||
| # on trader) sees the same string shape as the on-chain branch. | ||
| # Passing the full envelope here broke `_get_decision` — on tool | ||
| # failure the envelope JSON-parses but has no `p_yes`, hitting | ||
| # KeyError in `PredictionResponse.__init__`. | ||
| envelope = payload.get("response") | ||
| inner_result = ( | ||
| envelope.get("result") if isinstance(envelope, dict) else envelope | ||
| ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-blocking: if if isinstance(envelope, dict):
inner_result = envelope.get("result")
if inner_result is None:
self._logger.warning(
f"Offchain 'ok' response envelope missing 'result': {envelope!r}"
)
else:
inner_result = envelopeplus a test pinning the missing- |
||
| return _PollSnapshot( | ||
| status="ok", | ||
| result=self._serialise_result(payload.get("response")), | ||
| result=self._serialise_result(inner_result), | ||
| ) | ||
| if status == "rejected": | ||
| return _PollSnapshot( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,18 +147,60 @@ def test_5xx_counter_resets_on_success_status(self) -> None: | |
| assert snapshot.status == "ok" | ||
| assert snapshot.result == "answer" | ||
|
|
||
| def test_ok_status_returns_serialised_result(self) -> None: | ||
| """A 200 with ``status="ok"`` surfaces the serialised result. | ||
| def test_ok_status_extracts_inner_result_field(self) -> None: | ||
| """A 200 with ``status="ok"`` surfaces only the envelope's ``result``. | ||
|
|
||
| The real mech wraps the tool payload in a ``response`` envelope | ||
| (``{schema_version, requestId, result, tool, executed_at}``). To match | ||
| the on-chain ``mech_response`` ApiSpec (``response_key: result``), we | ||
| extract just the inner ``result`` field so ``DecisionReceive`` on | ||
| trader sees the same string shape as the on-chain branch. | ||
| """ | ||
| body = json.dumps( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this envelope dict is duplicated near-verbatim in the test below (only the def _ok_envelope_body(result: str) -> bytes:
"""Build a 200 ``status=ok`` body wrapping ``result`` in the mech envelope."""
return json.dumps(
{
"status": "ok",
"response": {
"schema_version": "2.0",
"requestId": "42",
"result": result,
"tool": "prediction-online",
"executed_at": "2026-07-02T14:24:27Z",
},
}
).encode()Keep the two tests separate (they pin distinct regressions). Separately: the deleted |
||
| { | ||
| "status": "ok", | ||
| "response": { | ||
| "schema_version": "2.0", | ||
| "requestId": "42", | ||
| "result": '{"p_yes": 0.6, "p_no": 0.4}', | ||
| "tool": "prediction-online", | ||
| "executed_at": "2026-07-02T14:24:27Z", | ||
| }, | ||
| } | ||
| ).encode() | ||
| stub = _StubBehaviour(http_responses=[_http_response(200, body)]) | ||
| poller = OffchainResponsePoller(stub) # type: ignore[arg-type] | ||
| snapshot = _drive(poller._poll_until_terminal("https://m", "42")) | ||
| assert snapshot.status == "ok" | ||
| assert snapshot.result == '{"p_yes": 0.6, "p_no": 0.4}' | ||
|
|
||
| def test_ok_status_passes_invalid_response_string_through_unwrapped( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: "passes ... through unwrapped" is easy to misread as exercising the |
||
| self, | ||
| ) -> None: | ||
| """Tool-failure envelopes carry a non-JSON ``result`` (``"Invalid response"``). | ||
|
|
||
| Non-string ``response`` values are JSON-serialised for the | ||
| downstream consumer's wire shape. | ||
| The downstream ``_get_decision`` in trader parses ``mech_response.result`` | ||
| with ``json.loads`` and catches ``JSONDecodeError`` to safely skip. If we | ||
| emitted the full envelope here, that catch would miss and the trader | ||
| would blow up in ``PredictionResponse(**dict)`` with ``KeyError('p_yes')``. | ||
| """ | ||
| body = json.dumps({"status": "ok", "response": {"answer": 1}}).encode() | ||
| body = json.dumps( | ||
| { | ||
| "status": "ok", | ||
| "response": { | ||
| "schema_version": "2.0", | ||
| "requestId": "42", | ||
| "result": "Invalid response", | ||
| "tool": "prediction-online", | ||
| "executed_at": "2026-07-02T14:24:27Z", | ||
| }, | ||
| } | ||
| ).encode() | ||
| stub = _StubBehaviour(http_responses=[_http_response(200, body)]) | ||
| poller = OffchainResponsePoller(stub) # type: ignore[arg-type] | ||
| snapshot = _drive(poller._poll_until_terminal("https://m", "42")) | ||
| assert snapshot.status == "ok" | ||
| assert json.loads(snapshot.result or "{}") == {"answer": 1} | ||
| assert snapshot.result == "Invalid response" | ||
|
|
||
| def test_rejected_status_surfaces_reason(self) -> None: | ||
| """A 200 with ``status="rejected"`` carries the reason as ``error``.""" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit (comment-rot risk): the
DecisionReceive/_get_decision/PredictionResponse.__init__/KeyError('p_yes')references describe trader-repo internals that nothing in this repo can verify or CI-pin — if trader refactors_get_decision, this goes stale silently. The ApiSpec half of the comment is verifiable here and is the load-bearing part; consider softening the trader specifics to "e.g. trader'sDecisionReceive(as of trader#1002)" so a future reader knows it's a snapshot, not a contract. Also: theelse envelopefallback branch (bare-string legacy probe, exercised bytest_5xx_counter_resets_on_success_status) isn't mentioned in the comment at all — one line saying why the non-dict passthrough exists would help.