gcf-python 2.6.0__tar.gz → 2.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {gcf_python-2.6.0 → gcf_python-2.7.0}/CHANGELOG.md +5 -0
  2. {gcf_python-2.6.0 → gcf_python-2.7.0}/PKG-INFO +20 -2
  3. {gcf_python-2.6.0 → gcf_python-2.7.0}/README.md +17 -1
  4. {gcf_python-2.6.0 → gcf_python-2.7.0}/pyproject.toml +8 -1
  5. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/__init__.py +1 -1
  6. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/decode_generic.py +21 -1
  7. gcf_python-2.7.0/src/gcf/fastmcp.py +131 -0
  8. gcf_python-2.7.0/tests/test_fastmcp.py +181 -0
  9. {gcf_python-2.6.0 → gcf_python-2.7.0}/.github/FUNDING.yml +0 -0
  10. {gcf_python-2.6.0 → gcf_python-2.7.0}/.github/workflows/ci.yml +0 -0
  11. {gcf_python-2.6.0 → gcf_python-2.7.0}/.github/workflows/publish.yml +0 -0
  12. {gcf_python-2.6.0 → gcf_python-2.7.0}/.gitignore +0 -0
  13. {gcf_python-2.6.0 → gcf_python-2.7.0}/LICENSE +0 -0
  14. {gcf_python-2.6.0 → gcf_python-2.7.0}/assets/divider-wave-2.png +0 -0
  15. {gcf_python-2.6.0 → gcf_python-2.7.0}/assets/divider.png +0 -0
  16. {gcf_python-2.6.0 → gcf_python-2.7.0}/assets/gcf-hero-wire-delta.png +0 -0
  17. {gcf_python-2.6.0 → gcf_python-2.7.0}/assets/gcf-python-diagram.png +0 -0
  18. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/__main__.py +0 -0
  19. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/cli.py +0 -0
  20. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/constants.py +0 -0
  21. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/decode.py +0 -0
  22. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/delta.py +0 -0
  23. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/encode.py +0 -0
  24. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/generic.py +0 -0
  25. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/generic_delta.py +0 -0
  26. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/keyed_map.py +0 -0
  27. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/packroot.py +0 -0
  28. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/scalar.py +0 -0
  29. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/session.py +0 -0
  30. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/stream.py +0 -0
  31. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/stream_generic.py +0 -0
  32. {gcf_python-2.6.0 → gcf_python-2.7.0}/src/gcf/types.py +0 -0
  33. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/__init__.py +0 -0
  34. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_conformance_v2.py +0 -0
  35. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_decode.py +0 -0
  36. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_delta.py +0 -0
  37. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_encode.py +0 -0
  38. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_generic.py +0 -0
  39. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_generic_delta.py +0 -0
  40. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_generic_delta_fuzz.py +0 -0
  41. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_generic_delta_session.py +0 -0
  42. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_keyed_map_fuzz.py +0 -0
  43. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_roundtrip.py +0 -0
  44. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_roundtrip_v2.py +0 -0
  45. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_session.py +0 -0
  46. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_stream.py +0 -0
  47. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_stream_fielddecl.py +0 -0
  48. {gcf_python-2.6.0 → gcf_python-2.7.0}/tests/test_stream_generic.py +0 -0
  49. {gcf_python-2.6.0 → gcf_python-2.7.0}/uv.lock +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.7.0 (2026-08-15)
4
+
5
+ - Decode: quoted-key/array-value round-trip (SPEC 4.2).
6
+ - Added: `gcf.fastmcp.GcfResponseMiddleware`, opt-in GCF encoding for FastMCP responses.
7
+
3
8
  ## v2.6.0 (2026-08-14)
4
9
 
5
10
  - **Numeric domain (spec v3.5.3, SPEC 2.3.2).** Specifies the canonical numeric domain as signed `int64` for integers and IEEE-754 double for non-integers. Earlier versions left integers beyond the double-exact range (2^53) to the host numeric type; this version parses integer literals to an exact `int64` on decode and on the JSON-to-value bridge, returns an out-of-range error for a value outside `int64` on both decode and encode, and models larger values (unsigned-64 identifiers, exact decimals) as strings. Canonical number formatting aligns to the domain: a double at or above 2^53 renders in exponent notation. Verified against new `numbers/017-024` and `errors-v2/041-042` conformance fixtures and the cross-SDK differential fuzz. An out-of-range value raises `ValueError`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gcf-python
3
- Version: 2.6.0
3
+ Version: 2.7.0
4
4
  Summary: The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies.
5
5
  Project-URL: Homepage, https://github.com/blackwell-systems/gcf-python
6
6
  Project-URL: Documentation, https://gcformat.com/
@@ -21,6 +21,8 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Topic :: Software Development :: Libraries
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.9
24
+ Provides-Extra: fastmcp
25
+ Requires-Dist: fastmcp>=2.11.0; extra == 'fastmcp'
24
26
  Description-Content-Type: text/markdown
25
27
 
26
28
  <p align="center">
@@ -244,6 +246,22 @@ for snapshot in stream: # each turn's current GenericSet
244
246
 
245
247
  `fixed_n(15)` re-anchors every N turns; `size_guard()` (recommended) re-anchors once the cumulative delta reaches a full payload's size. It introduces no new wire syntax and the decoder stays cadence-agnostic, so a re-anchor is just the protocol's "full" outcome on a schedule.
246
248
 
249
+ ## FastMCP middleware
250
+
251
+ A drop-in middleware for [FastMCP](https://gofastmcp.com) servers that re-encodes JSON tool results as GCF, opt-in. It requires the `fastmcp` extra (the core package stays zero-dependency):
252
+
253
+ ```bash
254
+ pip install "gcf-python[fastmcp]"
255
+ ```
256
+
257
+ ```python
258
+ from gcf.fastmcp import GcfResponseMiddleware
259
+
260
+ mcp.add_middleware(GcfResponseMiddleware())
261
+ ```
262
+
263
+ When `RESPONSE_FORMAT=gcf` is set in the environment, each tool result whose model-facing content is a single JSON text block is returned as a GCF generic wire instead of JSON. It is opt-in (nothing changes unless the variable is set), lossless and fail-safe (on any encoding error the original result is returned), and non-destructive: only a lone JSON text block is re-encoded, and the tool's `structuredContent` is preserved so output-schema validation and non-model clients keep receiving JSON.
264
+
247
265
  ## API
248
266
 
249
267
  | Function | Description |
@@ -304,7 +322,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
304
322
 
305
323
  **Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
306
324
 
307
- **Specification:** [SPEC v3.5.2 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 269 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.6.2, TypeScript v2.5.2, Python v2.5.3, Rust v2.5.3, Swift v2.6.2, Kotlin v2.5.2, .NET v0.1.2. Cross-language conformance verified across all seven SDKs.
325
+ **Specification:** [SPEC v3.5.3 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 279 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.7.0, TypeScript v2.6.0, Python v2.6.0, Rust v3.0.0, Swift v2.7.0, Kotlin v2.6.0, .NET v0.2.0. Cross-language conformance verified across all seven SDKs.
308
326
 
309
327
  ## Adopted by
310
328
 
@@ -219,6 +219,22 @@ for snapshot in stream: # each turn's current GenericSet
219
219
 
220
220
  `fixed_n(15)` re-anchors every N turns; `size_guard()` (recommended) re-anchors once the cumulative delta reaches a full payload's size. It introduces no new wire syntax and the decoder stays cadence-agnostic, so a re-anchor is just the protocol's "full" outcome on a schedule.
221
221
 
222
+ ## FastMCP middleware
223
+
224
+ A drop-in middleware for [FastMCP](https://gofastmcp.com) servers that re-encodes JSON tool results as GCF, opt-in. It requires the `fastmcp` extra (the core package stays zero-dependency):
225
+
226
+ ```bash
227
+ pip install "gcf-python[fastmcp]"
228
+ ```
229
+
230
+ ```python
231
+ from gcf.fastmcp import GcfResponseMiddleware
232
+
233
+ mcp.add_middleware(GcfResponseMiddleware())
234
+ ```
235
+
236
+ When `RESPONSE_FORMAT=gcf` is set in the environment, each tool result whose model-facing content is a single JSON text block is returned as a GCF generic wire instead of JSON. It is opt-in (nothing changes unless the variable is set), lossless and fail-safe (on any encoding error the original result is returned), and non-destructive: only a lone JSON text block is re-encoded, and the tool's `structuredContent` is preserved so output-schema validation and non-model clients keep receiving JSON.
237
+
222
238
  ## API
223
239
 
224
240
  | Function | Description |
@@ -279,7 +295,7 @@ GCF wins 15/16 datasets on the expanded [token efficiency benchmark](https://git
279
295
 
280
296
  **Zero runtime dependencies. Permanently.** All six implementations depend only on their language's standard library. No transitive dependencies. No supply chain risk. This is a permanent commitment: GCF will never take on external runtime dependencies. MIT licensed. All implementations support both generic profile (`encodeGeneric`) and graph profile (`encode`). CLI included in all 6 languages.
281
297
 
282
- **Specification:** [SPEC v3.5.2 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 269 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.6.2, TypeScript v2.5.2, Python v2.5.3, Rust v2.5.3, Swift v2.6.2, Kotlin v2.5.2, .NET v0.1.2. Cross-language conformance verified across all seven SDKs.
298
+ **Specification:** [SPEC v3.5.3 Stable](https://github.com/blackwell-systems/gcf/blob/main/SPEC.md) with 279 conformance fixtures, 43,000,000,000+ lossless round-trips verified across 5 formats and 6 languages. Current versions: Go v1.7.0, TypeScript v2.6.0, Python v2.6.0, Rust v3.0.0, Swift v2.7.0, Kotlin v2.6.0, .NET v0.2.0. Cross-language conformance verified across all seven SDKs.
283
299
 
284
300
  ## Adopted by
285
301
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "gcf-python"
7
- version = "2.6.0"
7
+ version = "2.7.0"
8
8
  description = "The AI-native wire format for structured data. 50-92% fewer tokens than JSON, with multi-turn delta encoding for agent loops. 100% comprehension on every frontier model. Zero dependencies."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -27,6 +27,13 @@ classifiers = [
27
27
  "Typing :: Typed",
28
28
  ]
29
29
 
30
+ # The core package is zero-dependency. The optional 'fastmcp' extra pulls FastMCP
31
+ # only for the gcf.fastmcp integration middleware (import gcf.fastmcp requires it).
32
+ [project.optional-dependencies]
33
+ fastmcp = [
34
+ "fastmcp>=2.11.0",
35
+ ]
36
+
30
37
  [project.scripts]
31
38
  gcf = "gcf.cli:main"
32
39
 
@@ -102,4 +102,4 @@ __all__ = [
102
102
  "DEFAULT_REANCHOR_N",
103
103
  ]
104
104
 
105
- __version__ = "2.6.0"
105
+ __version__ = "2.7.0"
@@ -171,7 +171,7 @@ def _parse_object_body(
171
171
 
172
172
  # Inline array (e.g. items[3]: a,b,c). Only reached if no = found.
173
173
  if not content.startswith("@") and not content.startswith("##"):
174
- bracket_idx = content.find("[")
174
+ bracket_idx = _find_array_bracket(content)
175
175
  if bracket_idx > 0:
176
176
  rest = content[bracket_idx:]
177
177
  close_idx = rest.find("]")
@@ -196,6 +196,26 @@ def _parse_object_body(
196
196
  return i - start
197
197
 
198
198
 
199
+ def _find_array_bracket(s: str) -> int:
200
+ """Index of the '[' that opens a named-array marker (``key[N]: ...``).
201
+
202
+ A quoted key is scanned first so a '[' inside the key name is not mistaken
203
+ for the array bracket (bare keys cannot contain '['). Returns -1 when the
204
+ key is a quoted string not immediately followed by '['.
205
+ """
206
+ if s and s[0] == '"':
207
+ i = 1
208
+ while i < len(s):
209
+ if s[i] == "\\":
210
+ i += 2
211
+ continue
212
+ if s[i] == '"':
213
+ return i + 1 if i + 1 < len(s) and s[i + 1] == "[" else -1
214
+ i += 1
215
+ return -1
216
+ return s.find("[")
217
+
218
+
199
219
  def _find_kv_split(s: str) -> int:
200
220
  if not s:
201
221
  return -1
@@ -0,0 +1,131 @@
1
+ """Optional FastMCP middleware for opt-in GCF response encoding.
2
+
3
+ This is a small integration helper for `FastMCP <https://gofastmcp.com>`_ servers.
4
+ It requires the ``fastmcp`` extra; the core ``gcf`` package stays
5
+ zero-dependency::
6
+
7
+ pip install "gcf-python[fastmcp]"
8
+
9
+ Register it once on a FastMCP server::
10
+
11
+ from gcf.fastmcp import GcfResponseMiddleware
12
+
13
+ mcp.add_middleware(GcfResponseMiddleware())
14
+
15
+ When ``RESPONSE_FORMAT=gcf`` is set in the environment, each tool result whose
16
+ model-facing content is a single JSON text block is re-encoded as a GCF generic
17
+ wire, so the response uses fewer tokens when it crosses the LLM boundary. It is:
18
+
19
+ - **Opt-in** — nothing changes unless ``RESPONSE_FORMAT=gcf`` is set (or
20
+ ``enabled=True`` is passed).
21
+ - **Lossless and fail-safe** — on any encoding error the original result is
22
+ returned, so a tool call is never dropped over formatting.
23
+ - **Non-destructive** — only a lone JSON text block is re-encoded; a result
24
+ carrying an image or any second block is left untouched, and the tool's
25
+ ``structuredContent`` (if any) is preserved so output-schema validation and
26
+ non-model clients keep receiving JSON.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import logging
33
+ import os
34
+ from typing import Any, Optional
35
+
36
+ try:
37
+ from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
38
+ from fastmcp.tools.tool import ToolResult
39
+ from mcp.types import TextContent
40
+ except ImportError as exc: # pragma: no cover - exercised only without the extra
41
+ raise ImportError(
42
+ "gcf.fastmcp requires the optional 'fastmcp' dependency: "
43
+ 'pip install "gcf-python[fastmcp]"'
44
+ ) from exc
45
+
46
+ from .decode_generic import decode_generic
47
+ from .generic import encode_generic
48
+
49
+ logger = logging.getLogger(__name__)
50
+
51
+ DEFAULT_ENV_VAR = "RESPONSE_FORMAT"
52
+
53
+
54
+ def gcf_response_enabled(env_var: str = DEFAULT_ENV_VAR) -> bool:
55
+ """Return True when ``env_var`` is set to ``gcf`` in the environment."""
56
+ return os.environ.get(env_var, "").strip().lower() == "gcf"
57
+
58
+
59
+ class GcfResponseMiddleware(Middleware):
60
+ """FastMCP middleware that re-encodes JSON tool results as GCF, opt-in.
61
+
62
+ Args:
63
+ env_var: Environment variable that gates encoding. Defaults to
64
+ ``RESPONSE_FORMAT``; encoding is on when its value is ``gcf``.
65
+ enabled: Force encoding on or off, bypassing the environment gate.
66
+ ``None`` (the default) reads the environment on each call.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ *,
72
+ env_var: str = DEFAULT_ENV_VAR,
73
+ enabled: Optional[bool] = None,
74
+ ) -> None:
75
+ self._env_var = env_var
76
+ self._enabled = enabled
77
+
78
+ def _is_enabled(self) -> bool:
79
+ if self._enabled is not None:
80
+ return self._enabled
81
+ return gcf_response_enabled(self._env_var)
82
+
83
+ async def on_call_tool(
84
+ self,
85
+ context: "MiddlewareContext",
86
+ call_next: "CallNext",
87
+ ) -> "ToolResult":
88
+ result = await call_next(context)
89
+
90
+ if not self._is_enabled():
91
+ return result
92
+
93
+ payload = _json_payload(result)
94
+ if payload is None:
95
+ return result
96
+
97
+ try:
98
+ wire = encode_generic(payload)
99
+ # Verify the wire decodes back to the same value before shipping it, so a
100
+ # result is never replaced with an unparseable or lossy encoding.
101
+ if decode_generic(wire) != payload:
102
+ return result
103
+ except Exception as exc: # noqa: BLE001 - liveness over correctness of format
104
+ logger.debug("GCF encoding skipped: %s", exc)
105
+ return result
106
+
107
+ return ToolResult(
108
+ content=[TextContent(type="text", text=wire)],
109
+ structured_content=result.structured_content,
110
+ )
111
+
112
+
113
+ def _json_payload(result: "ToolResult") -> Optional[Any]:
114
+ """The JSON value to encode, or None if the result is not a single JSON body.
115
+
116
+ Only a result whose content is exactly one text block is re-encoded, so an
117
+ image or other block sent alongside it is never dropped. ``structuredContent``
118
+ (the tool's typed value) is preferred as the payload; otherwise the lone text
119
+ block is parsed as JSON.
120
+ """
121
+ content = result.content or []
122
+ if len(content) != 1 or not isinstance(content[0], TextContent):
123
+ return None
124
+
125
+ if result.structured_content is not None:
126
+ return result.structured_content
127
+
128
+ try:
129
+ return json.loads(content[0].text)
130
+ except (json.JSONDecodeError, ValueError):
131
+ return None
@@ -0,0 +1,181 @@
1
+ """Tests for the optional gcf.fastmcp integration middleware.
2
+
3
+ Skipped cleanly when the 'fastmcp' extra is not installed.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import random
9
+ from types import SimpleNamespace
10
+
11
+ import pytest
12
+
13
+ pytest.importorskip("fastmcp")
14
+
15
+ from fastmcp.tools.tool import ToolResult # noqa: E402
16
+ from mcp.types import TextContent # noqa: E402
17
+
18
+ from gcf import decode_generic # noqa: E402
19
+ from gcf.fastmcp import GcfResponseMiddleware, gcf_response_enabled # noqa: E402
20
+
21
+
22
+ def _run(middleware, result):
23
+ async def call_next(_context):
24
+ return result
25
+
26
+ return asyncio.run(middleware.on_call_tool(SimpleNamespace(), call_next))
27
+
28
+
29
+ def _result(*content, structured_content=None):
30
+ return ToolResult(content=list(content), structured_content=structured_content)
31
+
32
+
33
+ def _text(result):
34
+ return result.content[0].text
35
+
36
+
37
+ def _docs(n):
38
+ return [{"id": i, "name": f"n{i}", "role": "admin" if i % 2 else "user"} for i in range(n)]
39
+
40
+
41
+ def test_gcf_response_enabled_reads_env(monkeypatch):
42
+ monkeypatch.delenv("RESPONSE_FORMAT", raising=False)
43
+ assert gcf_response_enabled() is False
44
+ for value in ("gcf", "GCF", " gcf "):
45
+ monkeypatch.setenv("RESPONSE_FORMAT", value)
46
+ assert gcf_response_enabled() is True
47
+ monkeypatch.setenv("RESPONSE_FORMAT", "json")
48
+ assert gcf_response_enabled() is False
49
+
50
+
51
+ def test_encodes_structured_content_as_gcf():
52
+ data = {"rows": _docs(20)}
53
+ result = _result(TextContent(type="text", text=json.dumps(data)), structured_content=data)
54
+
55
+ out = _run(GcfResponseMiddleware(enabled=True), result)
56
+
57
+ assert _text(out).startswith("GCF profile=generic")
58
+ assert decode_generic(_text(out)) == data # lossless round-trip
59
+ assert out.structured_content == data # preserved for non-model clients
60
+
61
+
62
+ def test_encodes_json_text_when_no_structured_content():
63
+ data = _docs(20)
64
+ result = _result(TextContent(type="text", text=json.dumps(data)))
65
+
66
+ out = _run(GcfResponseMiddleware(enabled=True), result)
67
+
68
+ assert _text(out).startswith("GCF profile=generic")
69
+ assert decode_generic(_text(out)) == data
70
+
71
+
72
+ def test_disabled_leaves_result_unchanged():
73
+ text = json.dumps(_docs(20))
74
+ result = _result(TextContent(type="text", text=text))
75
+
76
+ out = _run(GcfResponseMiddleware(enabled=False), result)
77
+
78
+ assert _text(out) == text
79
+
80
+
81
+ def test_env_gate(monkeypatch):
82
+ text = json.dumps(_docs(20))
83
+ monkeypatch.delenv("RESPONSE_FORMAT", raising=False)
84
+ out = _run(GcfResponseMiddleware(), _result(TextContent(type="text", text=text)))
85
+ assert _text(out) == text # env not set -> unchanged
86
+
87
+ monkeypatch.setenv("RESPONSE_FORMAT", "gcf")
88
+ out = _run(GcfResponseMiddleware(), _result(TextContent(type="text", text=text)))
89
+ assert _text(out).startswith("GCF profile=generic")
90
+
91
+
92
+ def test_non_json_text_unchanged():
93
+ result = _result(TextContent(type="text", text="a plain non-JSON message"))
94
+ out = _run(GcfResponseMiddleware(enabled=True), result)
95
+ assert _text(out) == "a plain non-JSON message"
96
+
97
+
98
+ def test_multiple_content_blocks_unchanged():
99
+ text = json.dumps(_docs(20))
100
+ result = _result(
101
+ TextContent(type="text", text=text),
102
+ TextContent(type="text", text="second block"),
103
+ )
104
+
105
+ out = _run(GcfResponseMiddleware(enabled=True), result)
106
+
107
+ assert len(out.content) == 2
108
+ assert out.content[0].text == text # not rewritten (would drop the second block)
109
+
110
+
111
+ def test_encode_error_falls_back_to_json():
112
+ # 2**63 is outside GCF's canonical int64 domain, so encode_generic raises and
113
+ # the middleware returns the original JSON result rather than dropping the call.
114
+ data = {"seq": 2**63}
115
+ text = json.dumps(data)
116
+ result = _result(TextContent(type="text", text=text), structured_content=data)
117
+
118
+ out = _run(GcfResponseMiddleware(enabled=True), result)
119
+
120
+ assert _text(out) == text
121
+
122
+
123
+ # --- fuzz: the safety invariant on arbitrary JSON ---
124
+
125
+ # Characters that stress GCF's delimiter/quoting rules and unicode handling.
126
+ _CHARS = "abc 0|,\"\\\n\t\r:{}[]<>é☕日本語—"
127
+
128
+
129
+ def _rand_str(rng, lo=0, hi=12):
130
+ return "".join(rng.choice(_CHARS) for _ in range(rng.randint(lo, hi)))
131
+
132
+
133
+ def _rand_scalar(rng):
134
+ kind = rng.randint(0, 5)
135
+ if kind == 0:
136
+ return rng.randint(-1_000_000, 1_000_000)
137
+ if kind == 1:
138
+ return rng.choice([True, False, None])
139
+ if kind == 2:
140
+ return round(rng.uniform(-1e6, 1e6), 4)
141
+ if kind == 3:
142
+ # occasionally an integer outside the int64 domain -> encode must decline safely
143
+ return rng.choice([2**63, -(2**63) - 1, 10**25])
144
+ return _rand_str(rng)
145
+
146
+
147
+ def _rand_json(rng, depth=0):
148
+ if depth >= 4 or rng.random() < 0.35:
149
+ return _rand_scalar(rng)
150
+ kind = rng.randint(0, 2)
151
+ if kind == 0:
152
+ return [_rand_json(rng, depth + 1) for _ in range(rng.randint(0, 5))]
153
+ if kind == 1:
154
+ return {_rand_str(rng, 1, 8): _rand_json(rng, depth + 1) for _ in range(rng.randint(0, 5))}
155
+ # array of uniform records (GCF's favorable shape)
156
+ keys = [_rand_str(rng, 1, 6) for _ in range(rng.randint(1, 5))]
157
+ return [{k: _rand_json(rng, depth + 2) for k in keys} for _ in range(rng.randint(0, 8))]
158
+
159
+
160
+ def test_fuzz_middleware_never_grows_corrupts_or_crashes():
161
+ rng = random.Random(20260815)
162
+ middleware = GcfResponseMiddleware(enabled=True)
163
+
164
+ for _ in range(5000):
165
+ payload = _rand_json(rng)
166
+ text = json.dumps(payload)
167
+ structured = payload if isinstance(payload, dict) else None
168
+ result = _result(TextContent(type="text", text=text), structured_content=structured)
169
+
170
+ out = _run(middleware, result)
171
+
172
+ # Always exactly one text block; never dropped or multiplied.
173
+ assert len(out.content) == 1
174
+ got = out.content[0].text
175
+
176
+ if got == text:
177
+ continue # declined -> original JSON kept (always safe)
178
+
179
+ # Otherwise it MUST be a GCF wire that round-trips to the exact payload.
180
+ assert got.startswith("GCF profile=generic")
181
+ assert decode_generic(got) == payload
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes