Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nextgen_mcp/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ def query_files_by_selector(
query=query,
columns=list(df.columns),
rows=int(len(df)),
data=df.to_dict(orient="records"),
data=[_normalize_record(r) for r in df.to_dict(orient="records")],
)

# Surface the exclusion count when non-zero so the user/LLM has a
Expand Down
49 changes: 49 additions & 0 deletions test_mcp/test_query_files_by_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,55 @@ def test_index_filter_returns_single_file_at_index(monkeypatch):
assert captured["urls"][0].endswith("b.parquet")


# ---------------------------------------------------------------------------
# JSON-serialization contract
# ---------------------------------------------------------------------------


def test_nan_cells_serialize_to_valid_json(monkeypatch):
"""NaN cells in the query result MUST become JSON null, not literal NaN.

Backstory: troute parquet output contains NaN in the ``nudge`` column for
non-assimilated reaches. Without normalization, pandas keeps the value as
``float('nan')`` and Python's default ``json.dumps`` emits literal ``NaN``,
which (a) is not valid JSON and (b) makes the entire result envelope
un-parseable in JS clients. In chatbox-core, that string-failure path
bypasses ``_engine_dispatched`` + ``_cache_uri`` injection silently — so
the LLM tokenizes the full payload into the next tool call instead of
referencing the cache. Lock the contract: the envelope round-trips
through strict ``json.loads(json.dumps(...))``.
"""
import json

listing = [_full("a.parquet")]
_install_fs(monkeypatch, listing)
_install_fake_parquets_query(
monkeypatch,
lambda urls, q: pd.DataFrame(
[
{"flow": 0.5, "nudge": float("nan")},
{"flow": 0.6, "nudge": float("nan")},
{"flow": 0.7, "nudge": 0.01},
]
),
)

result = logic.query_files_by_selector(
**SELECTOR,
query="SELECT * FROM output",
)

assert result.get("ok") is True
# Strict round-trip — emits with allow_nan=False so any residual NaN
# surfaces as a ValueError rather than silently producing invalid JSON.
round_tripped = json.loads(json.dumps(result, allow_nan=False))
rows = round_tripped["data"]
assert rows[0]["nudge"] is None
assert rows[1]["nudge"] is None
assert rows[2]["nudge"] == 0.01
assert rows[0]["flow"] == 0.5


# ---------------------------------------------------------------------------
# XOR + Pydantic edge cases
# ---------------------------------------------------------------------------
Expand Down
Loading