tigrbl_tests 0.4.3.dev4__py3-none-any.whl → 0.4.4.dev7__py3-none-any.whl

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 (53) hide show
  1. tests/architecture/test_runtime_structure.py +6 -2
  2. tests/i9n/test_batch_scheduler_i9n.py +1 -1
  3. tests/i9n/test_resident_batch_scheduler_runtime.py +1 -1
  4. tests/i9n/test_schema_ctx_attributes_integration.py +3 -1
  5. tests/i9n/test_webtransport_tigrcorn_bridge.py +4 -2
  6. tests/i9n/test_webtransport_tigrcorn_session_multiplexing.py +18 -15
  7. tests/test_secdeps_execute_in_pre_tx.py +1 -1
  8. tests/unit/decorators/test_declarative_surface.py +3 -2
  9. tests/unit/decorators/test_schema_ctx_bindings.py +2 -0
  10. tests/unit/runtime/test_binding_exchange_normalization_contract.py +7 -7
  11. tests/unit/runtime/test_binding_token_lowering_contract.py +83 -8
  12. tests/unit/runtime/test_bindingspec_event_subevent_schema_contract.py +1 -1
  13. tests/unit/runtime/test_bindingspec_kernelplan_protocol_compilation_contract.py +58 -2
  14. tests/unit/runtime/test_canonical_bindingspec_framing_policy.py +50 -37
  15. tests/unit/runtime/test_contract_classification_consumption_policy.py +0 -2
  16. tests/unit/runtime/test_cross_transport_equivalence_contract.py +1 -1
  17. tests/unit/runtime/test_eventful_protocol_decorator_surface_contract.py +3 -1
  18. tests/unit/runtime/test_eventful_subevent_surface_contracts.py +8 -2
  19. tests/unit/runtime/test_framing_matrix_ssot_conformance.py +3 -3
  20. tests/unit/runtime/test_http_stream_client_stream_runtime_contract.py +2 -2
  21. tests/unit/runtime/test_op_verb_to_default_binding_matrix_contract.py +44 -1
  22. tests/unit/runtime/test_protocol_anchor_ordering_parity_contract.py +3 -5
  23. tests/unit/runtime/test_python_only_runtime_no_rust_public_exports.py +14 -0
  24. tests/unit/runtime/test_python_only_runtime_rust_executor_rejection.py +17 -31
  25. tests/unit/runtime/test_python_only_runtime_rust_kernel_module_retirement.py +9 -30
  26. tests/unit/runtime/test_python_only_runtime_ssot_docs_rust_parity_ban.py +2 -8
  27. tests/unit/runtime/test_runtime_frame_codec_contract.py +65 -27
  28. tests/unit/runtime/test_session_leakage_prevention_contract.py +1 -1
  29. tests/unit/runtime/test_stream_table_current_behavior_contract.py +145 -0
  30. tests/unit/runtime/test_table_profile_op_selection_matrix_contract.py +9 -3
  31. tests/unit/runtime/test_table_transport_binding_profiles_contract.py +24 -1
  32. tests/unit/runtime/test_transport_delivery_guarantees_contract.py +7 -3
  33. tests/unit/runtime/test_unsupported_framing_fail_closed_contract.py +18 -20
  34. tests/unit/runtime/test_websocket_framing_runtime_contract.py +18 -20
  35. tests/unit/runtime/test_webtransport_lane_framing_policy.py +28 -22
  36. tests/unit/test_handler_entrypoints.py +60 -0
  37. tests/unit/test_operator_surface_closure.py +2 -1
  38. tests/unit/test_operator_websocket_route_contracts.py +2 -1
  39. tests/unit/test_package_badges_and_notices.py +1 -1
  40. tests/unit/test_schema_ctx_attributes.py +29 -0
  41. tests/unit/test_schema_ctx_plain_class.py +2 -0
  42. tests/unit/test_spec_binding_app_engine_contracts.py +4 -2
  43. tests/unit/test_spec_contracts_session_bindings.py +5 -4
  44. {tigrbl_tests-0.4.3.dev4.dist-info → tigrbl_tests-0.4.4.dev7.dist-info}/METADATA +1 -1
  45. {tigrbl_tests-0.4.3.dev4.dist-info → tigrbl_tests-0.4.4.dev7.dist-info}/RECORD +48 -51
  46. tests/rust/atoms/test_rust_atoms_public_surface.py +0 -21
  47. tests/rust/ffi/test_rust_binding_trace.py +0 -21
  48. tests/rust/kernel/test_rust_kernel_public_surface.py +0 -17
  49. tests/rust/runtime/test_rust_runtime_engine_policy.py +0 -12
  50. tests/rust/runtime/test_rust_runtime_public_surface.py +0 -36
  51. {tigrbl_tests-0.4.3.dev4.dist-info → tigrbl_tests-0.4.4.dev7.dist-info}/WHEEL +0 -0
  52. {tigrbl_tests-0.4.3.dev4.dist-info → tigrbl_tests-0.4.4.dev7.dist-info}/licenses/LICENSE +0 -0
  53. {tigrbl_tests-0.4.3.dev4.dist-info → tigrbl_tests-0.4.4.dev7.dist-info}/licenses/NOTICE +0 -0
@@ -9,10 +9,14 @@ RUNTIME_PKG = _STANDARDS / "tigrbl_runtime" / "tigrbl_runtime"
9
9
 
10
10
  def test_dependency_invoke_is_runtime_event_anchor():
11
11
  system = (RUNTIME_PKG / "runtime" / "system.py").read_text()
12
- invoke = (RUNTIME_PKG / "executors" / "invoke.py").read_text()
12
+ phase_runner = (RUNTIME_PKG / "executors" / "phase_runner.py").read_text()
13
+ executors_init = (RUNTIME_PKG / "executors" / "__init__.py").read_text()
14
+ assert not (RUNTIME_PKG / "executors" / "invoke.py").exists()
13
15
  assert "DEP_EXTRA" in system
14
16
  assert '"PRE_TX_BEGIN"' in system
15
- assert '"PRE_TX_BEGIN"' in invoke
17
+ assert '"PRE_TX_BEGIN"' in phase_runner
18
+ assert "invoke_op" not in executors_init
19
+ assert "tigrbl_ops_oltp" not in phase_runner
16
20
 
17
21
 
18
22
  def test_runtime_gateway_owns_runtime_entrypoint_and_send():
@@ -53,7 +53,7 @@ async def test_batch_scheduler_i9n_executemany_to_grouped_fanout() -> None:
53
53
  db=Db(),
54
54
  op="create",
55
55
  model=object,
56
- batch_policy={"enabled": True, "max_size": 2},
56
+ batch_policy={"enabled": True, "max_size": 2, "max_delay_ms": 60_000},
57
57
  temp={},
58
58
  )
59
59
 
@@ -31,7 +31,7 @@ class Hot:
31
31
  batch = {
32
32
  "enabled": True,
33
33
  "max_size": 2,
34
- "max_delay_ms": 1,
34
+ "max_delay_ms": 60_000,
35
35
  "max_queue_depth": 8,
36
36
  }
37
37
 
@@ -55,9 +55,11 @@ async def test_schema_ctx_bindings(schema_ctx_client):
55
55
  @pytest.mark.i9n
56
56
  @pytest.mark.asyncio
57
57
  async def test_schema_ctx_request_response_schema(schema_ctx_client):
58
- _, router, _, _ = schema_ctx_client
58
+ _, router, Widget, _ = schema_ctx_client
59
59
  create_schema = router.schemas.Widget.create.in_
60
60
  read_schema = router.schemas.Widget.read.out
61
+ assert create_schema is Widget.schemas.create.in_
62
+ assert read_schema is Widget.schemas.read.out
61
63
  assert create_schema.model_fields["name"].is_required()
62
64
  assert "age" in read_schema.model_fields
63
65
 
@@ -7,6 +7,7 @@ from typing import Any
7
7
  import pytest
8
8
 
9
9
  from tigrbl import WebTransportBindingSpec
10
+ from tigrbl_core._spec import TextFramingSpec
10
11
  from tigrbl_core._spec.hook_spec import HookSpec
11
12
  from tigrbl_core._spec.hook_types import HookPhase
12
13
  from tigrbl_concrete._concrete._app import App as TigrblApp
@@ -77,7 +78,7 @@ def _app(path: str) -> TigrblApp:
77
78
  proto="webtransport",
78
79
  path=path,
79
80
  profile="bidi_stream",
80
- inner_framing="text",
81
+ inner_framing=TextFramingSpec(),
81
82
  ),
82
83
  tigrbl_exchange="bidirectional_stream",
83
84
  )
@@ -132,6 +133,7 @@ async def test_tigrbl_webtransport_bidi_stream_runs_over_tigrcorn_contract_event
132
133
  "session_id": "session-1",
133
134
  "stream_id": "stream-1",
134
135
  "stream_direction": "bidi",
136
+ "stream_initiator": "client",
135
137
  "framing": "text",
136
138
  "data": b"echo:hello",
137
139
  "more": False,
@@ -220,7 +222,7 @@ async def test_webtransport_bidi_and_unidi_lane_metadata_reaches_hooks() -> None
220
222
  proto="webtransport",
221
223
  path="/transport/lanes",
222
224
  profile="bidi_stream",
223
- inner_framing="text",
225
+ inner_framing=TextFramingSpec(),
224
226
  ),
225
227
  tigrbl_exchange="bidirectional_stream",
226
228
  )
@@ -8,6 +8,7 @@ from typing import Any
8
8
  import pytest
9
9
 
10
10
  from tigrbl import WebTransportBindingSpec
11
+ from tigrbl_core._spec import TextFramingSpec
11
12
  from tigrbl_core._spec.hook_spec import HookSpec
12
13
  from tigrbl_core._spec.hook_types import HookPhase
13
14
  from tigrbl_concrete._concrete._app import App as TigrblApp
@@ -87,7 +88,7 @@ def _app(path: str) -> TigrblApp:
87
88
  proto="webtransport",
88
89
  path=path,
89
90
  profile="bidi_stream",
90
- inner_framing="text",
91
+ inner_framing=TextFramingSpec(),
91
92
  ),
92
93
  tigrbl_exchange="bidirectional_stream",
93
94
  )
@@ -239,7 +240,7 @@ async def test_webtransport_tigrcorn_session_stays_open_for_delayed_lanes() -> N
239
240
  proto="webtransport",
240
241
  path="/transport/echo-live",
241
242
  profile="bidi_stream",
242
- inner_framing="text",
243
+ inner_framing=TextFramingSpec(),
243
244
  ),
244
245
  tigrbl_exchange="bidirectional_stream",
245
246
  )
@@ -266,20 +267,22 @@ async def test_webtransport_tigrcorn_session_stays_open_for_delayed_lanes() -> N
266
267
  assert stream_sends == [
267
268
  {
268
269
  "type": "webtransport.stream.send",
269
- "session_id": "session-1",
270
- "stream_id": "bidi-1",
271
- "stream_direction": "bidi",
272
- "framing": "text",
273
- "data": b"echo:alpha",
270
+ "session_id": "session-1",
271
+ "stream_id": "bidi-1",
272
+ "stream_direction": "bidi",
273
+ "stream_initiator": "client",
274
+ "framing": "text",
275
+ "data": b"echo:alpha",
274
276
  "more": False,
275
277
  },
276
278
  {
277
279
  "type": "webtransport.stream.send",
278
- "session_id": "session-1",
279
- "stream_id": "bidi-2",
280
- "stream_direction": "bidi",
281
- "framing": "text",
282
- "data": b"echo:beta",
280
+ "session_id": "session-1",
281
+ "stream_id": "bidi-2",
282
+ "stream_direction": "bidi",
283
+ "stream_initiator": "client",
284
+ "framing": "text",
285
+ "data": b"echo:beta",
283
286
  "more": False,
284
287
  },
285
288
  ]
@@ -297,11 +300,11 @@ async def test_webtransport_tigrcorn_session_stays_open_for_delayed_lanes() -> N
297
300
  assert [
298
301
  (item["direction"], item["type"], item.get("stream_id"), item.get("datagram_id"))
299
302
  for item in trace
300
- if item["direction"] == "receive"
303
+ if item["phase"] == "ctx.channel_message"
301
304
  ] == [
302
305
  ("receive", "webtransport.connect", None, None),
303
- ("receive", "webtransport.stream.receive", "bidi-1", None),
304
- ("receive", "webtransport.stream.receive", "bidi-2", None),
306
+ ("bidirectional", "webtransport.stream.receive", "bidi-1", None),
307
+ ("bidirectional", "webtransport.stream.receive", "bidi-2", None),
305
308
  ("receive", "webtransport.datagram.receive", None, "dg-client-1"),
306
309
  ("receive", "webtransport.disconnect", None, None),
307
310
  ]
@@ -8,7 +8,7 @@ from tigrbl._spec import OpSpec
8
8
  from tigrbl.runtime import events as _ev
9
9
  from tigrbl.runtime import system as _sys
10
10
  from tigrbl_kernel import Kernel
11
- from tigrbl_runtime.executors.invoke import invoke_op
11
+ from tigrbl_concrete._mapping.invoke import invoke_op
12
12
 
13
13
 
14
14
  class _FakeDB:
@@ -20,6 +20,7 @@ from tigrbl import (
20
20
  webtransport_ctx,
21
21
  )
22
22
  from tigrbl_core.config.constants import HOOK_DECLS_ATTR
23
+ from tigrbl_core._spec import JsonRpcFramingSpec, StreamFramingSpec
23
24
  from tests.conftest import mro_collect_decorated_ops
24
25
 
25
26
 
@@ -80,10 +81,10 @@ def test_alias_surface_decorators_attach_expected_bindings() -> None:
80
81
 
81
82
  specs = {spec.alias: spec for spec in mro_collect_decorated_ops(Widget)}
82
83
  assert isinstance(specs["socket"].bindings[0], WsBindingSpec)
83
- assert specs["socket"].bindings[0].framing == "jsonrpc"
84
+ assert specs["socket"].bindings[0].framing == JsonRpcFramingSpec()
84
85
  assert isinstance(specs["events"].bindings[0], SseBindingSpec)
85
86
  assert specs["events"].exchange == "server_stream"
86
- assert specs["stream"].bindings[0].framing == "stream"
87
+ assert specs["stream"].bindings[0].framing == StreamFramingSpec()
87
88
  assert specs["transport"].bindings[0].proto == "webtransport"
88
89
 
89
90
 
@@ -15,6 +15,7 @@ def test_schema_ctx_internal_binding_attaches_decl_to_schema():
15
15
  assert decl.alias == "Search"
16
16
  assert decl.kind == "in"
17
17
  assert not hasattr(Widget, TIGRBL_SCHEMA_DECLS_ATTR)
18
+ assert not hasattr(Widget, "schemas")
18
19
 
19
20
 
20
21
  def test_schema_ctx_external_binding_uses_for_argument():
@@ -27,6 +28,7 @@ def test_schema_ctx_external_binding_uses_for_argument():
27
28
 
28
29
  mapping = getattr(Gadget, TIGRBL_SCHEMA_DECLS_ATTR)
29
30
  assert mapping["Result"]["out"] is ResultSchema
31
+ assert Gadget.schemas.Result.out is ResultSchema
30
32
  decl = ResultSchema.__tigrbl_schema_decl__
31
33
  assert decl.alias == "Result"
32
34
  assert decl.kind == "out"
@@ -66,12 +66,12 @@ def test_transport_bindings_project_canonical_exchange_family_and_subevents() ->
66
66
  ),
67
67
  (
68
68
  binding_spec.HttpStreamBindingSpec(proto="http.stream", path="/items/stream"),
69
- {
70
- "proto": "http.stream",
71
- "exchange": "server_stream",
72
- "framing": "stream",
73
- "family": "stream",
74
- },
69
+ {
70
+ "proto": "http.stream",
71
+ "exchange": "server_stream",
72
+ "framing": "stream",
73
+ "family": "stream",
74
+ },
75
75
  ),
76
76
  (
77
77
  binding_spec.SseBindingSpec(path="/items/events"),
@@ -96,7 +96,7 @@ def test_transport_bindings_project_canonical_exchange_family_and_subevents() ->
96
96
  {
97
97
  "proto": "webtransport",
98
98
  "exchange": "bidirectional_stream",
99
- "framing": "webtransport",
99
+ "framing": "",
100
100
  "family": "session",
101
101
  },
102
102
  ),
@@ -20,6 +20,10 @@ from tigrbl import (
20
20
  )
21
21
  from tigrbl_core._spec import (
22
22
  HttpStreamBindingSpec,
23
+ BytesFramingSpec,
24
+ JsonFramingSpec,
25
+ JsonRpcFramingSpec,
26
+ NdjsonFramingSpec,
23
27
  OpSpec,
24
28
  TableProfileError,
25
29
  TableProfileSpec,
@@ -44,7 +48,7 @@ def test_binding_tokens_have_canonical_shape() -> None:
44
48
  assert token.op_alias == "create"
45
49
  assert token.op_target == "create"
46
50
  assert token.binding_kind == "http.rest"
47
- assert token.path == "/create"
51
+ assert token.path == "/resttable"
48
52
  assert token.methods == ("POST",)
49
53
  assert token.framing == "json"
50
54
  assert token.exchange == "request_response"
@@ -136,9 +140,9 @@ def test_oltp_and_olap_token_details_preserve_protocol_specific_selectors() -> N
136
140
  jsonrpc_olap = {token.op_alias: token for token in _tokens(JsonRpcOlapTable)}
137
141
 
138
142
  assert rest_oltp["merge"].methods == ("PATCH",)
139
- assert rest_oltp["merge"].path == "/merge"
140
- assert rest_olap["aggregate"].methods == ("GET",)
141
- assert rest_olap["aggregate"].path == "/aggregate"
143
+ assert rest_oltp["merge"].path == "/restoltptable/{item_id}"
144
+ assert rest_olap["aggregate"].methods == ("POST",)
145
+ assert rest_olap["aggregate"].path == "/restolaptable"
142
146
  assert jsonrpc_oltp["merge"].rpc_method == "JsonRpcOltpTable.merge"
143
147
  assert jsonrpc_oltp["merge"].framing == "jsonrpc"
144
148
  assert jsonrpc_olap["aggregate"].rpc_method == "JsonRpcOlapTable.aggregate"
@@ -152,7 +156,7 @@ def test_dual_tokens_keep_independent_rest_path_and_jsonrpc_method() -> None:
152
156
 
153
157
  merge = pairs["merge"]
154
158
 
155
- assert merge["http.rest"].path == "/merge"
159
+ assert merge["http.rest"].path == "/restjsonrpcoltptable/{item_id}"
156
160
  assert merge["http.rest"].methods == ("PATCH",)
157
161
  assert merge["http.rest"].rpc_method is None
158
162
  assert merge["http.jsonrpc"].path == ""
@@ -189,7 +193,7 @@ def test_rest_binding_tokens_include_method_path_and_media_type() -> None:
189
193
  assert tokens["update"].methods == ("PATCH",)
190
194
  assert tokens["replace"].methods == ("PUT",)
191
195
  assert tokens["delete"].methods == ("DELETE",)
192
- assert tokens["list"].path == "/list"
196
+ assert tokens["list"].path == "/resttable"
193
197
  assert tokens["list"].framing == "json"
194
198
 
195
199
 
@@ -205,7 +209,11 @@ def test_websocket_binding_tokens_include_framing_and_session_lanes() -> None:
205
209
  tokens = _tokens(WebSocketJsonRpcTable)
206
210
 
207
211
  assert {token.binding_kind for token in tokens} == {"ws"}
212
+ assert {token.protocol_kind for token in tokens} == {"ws"}
208
213
  assert {token.framing for token in tokens} == {"jsonrpc"}
214
+ assert {token.framing_kind for token in tokens} == {"jsonrpc"}
215
+ assert {token.framing_spec for token in tokens} == {"JsonRpcFramingSpec"}
216
+ assert {token.required_subprotocol for token in tokens} == {"jsonrpc"}
209
217
  assert {tuple(binding.subprotocols) for op in TableSpec.collect(WebSocketJsonRpcTable).ops for binding in op.bindings} == {
210
218
  ("jsonrpc",)
211
219
  }
@@ -221,6 +229,73 @@ def test_webtransport_binding_tokens_include_stream_and_datagram_lanes() -> None
221
229
  assert {token.op_target for token in datagram_tokens} == {"send_datagram"}
222
230
 
223
231
 
232
+ def test_webtransport_op_lane_binding_contract() -> None:
233
+ profile = TableProfileSpec(
234
+ kind="webtransport_ops",
235
+ ops=(
236
+ OpSpec(alias="create", target="create"),
237
+ OpSpec(alias="tail", target="tail"),
238
+ OpSpec(alias="append_chunk", target="append_chunk"),
239
+ OpSpec(alias="send_datagram", target="send_datagram"),
240
+ OpSpec(alias="open_unidi_stream", target="open_unidi_stream"),
241
+ ),
242
+ )
243
+
244
+ lowered = lower_table_profile_bindings(WebTransportBidiTable, profile, tuple(profile.ops))
245
+ by_target = {op.target: op.bindings[0] for op in lowered}
246
+
247
+ assert (by_target["create"].lane, by_target["create"].inner_framing) == (
248
+ "bidi_stream",
249
+ JsonRpcFramingSpec(),
250
+ )
251
+ assert (by_target["tail"].lane, by_target["tail"].inner_framing) == (
252
+ "unidi_server_stream",
253
+ NdjsonFramingSpec(),
254
+ )
255
+ assert (
256
+ by_target["append_chunk"].lane,
257
+ by_target["append_chunk"].inner_framing,
258
+ ) == ("unidi_client_stream", BytesFramingSpec())
259
+ assert (
260
+ by_target["send_datagram"].lane,
261
+ by_target["send_datagram"].inner_framing,
262
+ ) == ("datagram", JsonFramingSpec())
263
+ assert (
264
+ by_target["open_unidi_stream"].lane,
265
+ by_target["open_unidi_stream"].inner_framing,
266
+ ) == ("bidi_stream", JsonRpcFramingSpec())
267
+
268
+
269
+ def test_canonical_binding_token_typed_framing_fields() -> None:
270
+ profile = TableProfileSpec(
271
+ kind="tests.explicit-ws-jsonrpc",
272
+ ops=(
273
+ OpSpec(
274
+ alias="create",
275
+ target="create",
276
+ bindings=(
277
+ WebTransportBindingSpec(
278
+ profile="bidi_stream",
279
+ inner_framing=JsonRpcFramingSpec(),
280
+ ),
281
+ ),
282
+ ),
283
+ ),
284
+ custom=True,
285
+ namespace="tests",
286
+ )
287
+ op = tuple(profile.ops)[0]
288
+
289
+ token = lower_binding_tokens_for_ops(WebTransportBidiTable, profile, (op,))[0]
290
+
291
+ assert token.protocol_kind == "webtransport"
292
+ assert token.framing == ""
293
+ assert token.framing_kind == ""
294
+ assert token.framing_spec == ""
295
+ assert token.lane == "bidi_stream"
296
+ assert token.inner_framing == "jsonrpc"
297
+
298
+
224
299
  def test_binding_token_lowering_rejects_unsupported_transport_profile_pairs() -> None:
225
300
  profile = make_table_profile("stream", ())
226
301
  op = OpSpec(alias="create", target="create")
@@ -230,8 +305,8 @@ def test_binding_token_lowering_rejects_unsupported_transport_profile_pairs() ->
230
305
 
231
306
 
232
307
  def test_binding_token_lowering_rejects_unsupported_framing_fallback() -> None:
233
- with pytest.raises(ValueError, match="inner framing"):
234
- WebTransportBindingSpec(profile="datagram", inner_framing="jsonrpc")
308
+ with pytest.raises(TypeError, match="FramingSpec"):
309
+ WebTransportBindingSpec(profile="datagram", inner_framing="ndjson")
235
310
 
236
311
 
237
312
  def test_binding_token_lowering_reports_source_precedence() -> None:
@@ -69,7 +69,7 @@ def _require(module_name: str, attr_name: str):
69
69
  {
70
70
  "exchange": "bidirectional_stream",
71
71
  "family": "session",
72
- "framing": "webtransport",
72
+ "framing": "",
73
73
  "subevents": ("session.open", "stream.received", "datagram.received", "session.close"),
74
74
  },
75
75
  ),
@@ -4,6 +4,8 @@ import importlib
4
4
 
5
5
  import pytest
6
6
 
7
+ from tigrbl_core._spec.binding_spec import JsonRpcFramingSpec
8
+
7
9
 
8
10
  def _require(module_name: str, attr_name: str):
9
11
  try:
@@ -29,7 +31,7 @@ def _require(module_name: str, attr_name: str):
29
31
  ),
30
32
  (
31
33
  {"kind": "http.stream", "path": "/stream"},
32
- {"family": "stream", "framing": "stream", "anchors": ("transport.emit", "transport.emit_complete")},
34
+ {"family": "stream", "framing": "bytes", "anchors": ("transport.emit", "transport.emit_complete")},
33
35
  ),
34
36
  (
35
37
  {"kind": "http.sse", "path": "/events"},
@@ -41,7 +43,7 @@ def _require(module_name: str, attr_name: str):
41
43
  ),
42
44
  (
43
45
  {"kind": "webtransport", "path": "/transport"},
44
- {"family": "session", "framing": "webtransport", "anchors": ("transport.accept", "dispatch.subevent.derive", "transport.close")},
46
+ {"family": "session", "framing": "", "anchors": ("transport.accept", "dispatch.subevent.derive", "transport.close")},
45
47
  ),
46
48
  ),
47
49
  )
@@ -71,6 +73,59 @@ def test_protocol_compilation_is_deterministic_for_equivalent_bindings() -> None
71
73
  assert first == second
72
74
 
73
75
 
76
+ def test_kernel_protocol_plan_accepts_typed_websocket_framing_spec() -> None:
77
+ compile_binding = _require("tigrbl_kernel.protocol_bindings", "compile_binding_protocol_plan")
78
+
79
+ plan = compile_binding(
80
+ op_id="Socket.rpc",
81
+ binding={"kind": "ws", "path": "/socket", "framing": JsonRpcFramingSpec()},
82
+ )
83
+
84
+ assert plan["framing"] == "jsonrpc"
85
+ assert plan["framing_kind"] == "jsonrpc"
86
+ assert plan["framing_spec"] == "JsonRpcFramingSpec"
87
+ assert plan["websocket_subprotocol"] == "jsonrpc"
88
+ assert "required_subprotocol" not in plan
89
+ assert "subprotocols" not in plan
90
+ assert plan["event_key_inputs"]["websocket_subprotocol"] == "jsonrpc"
91
+
92
+
93
+ def test_kernel_protocol_plan_rejects_conflicting_websocket_subprotocol() -> None:
94
+ compile_binding = _require("tigrbl_kernel.protocol_bindings", "compile_binding_protocol_plan")
95
+
96
+ with pytest.raises(ValueError, match="conflicts with subprotocols"):
97
+ compile_binding(
98
+ op_id="Socket.rpc",
99
+ binding={
100
+ "kind": "ws",
101
+ "path": "/socket",
102
+ "framing": JsonRpcFramingSpec(),
103
+ "subprotocols": ("graphql-ws",),
104
+ },
105
+ )
106
+
107
+
108
+ def test_kernel_protocol_plan_normalizes_webtransport_inner_framing_spec() -> None:
109
+ compile_binding = _require("tigrbl_kernel.protocol_bindings", "compile_binding_protocol_plan")
110
+
111
+ plan = compile_binding(
112
+ op_id="Transport.create",
113
+ binding={
114
+ "kind": "webtransport",
115
+ "path": "/wt",
116
+ "profile": "bidi_stream",
117
+ "inner_framing": JsonRpcFramingSpec(),
118
+ },
119
+ )
120
+
121
+ assert plan["framing"] == "jsonrpc"
122
+ assert plan["framing_kind"] == "jsonrpc"
123
+ assert plan["framing_spec"] == "JsonRpcFramingSpec"
124
+ assert plan["inner_framing"] == "jsonrpc"
125
+ assert plan["inner_framing_spec"] == "JsonRpcFramingSpec"
126
+ assert plan["event_key_inputs"]["inner_framing"] == "jsonrpc"
127
+
128
+
74
129
  def test_protocol_compilation_emits_lifecycle_matrix_rows_for_each_subevent() -> None:
75
130
  compile_binding = _require("tigrbl_kernel.protocol_bindings", "compile_binding_protocol_plan")
76
131
 
@@ -98,6 +153,7 @@ def test_protocol_compilation_uses_bindingspec_as_source_of_truth_not_transport_
98
153
  {"kind": "http.rest", "path": "/items", "framing": "sse"},
99
154
  {"kind": "http.jsonrpc", "path": "/rpc"},
100
155
  {"kind": "ws", "path": "/socket", "methods": ("GET",)},
156
+ {"kind": "webtransport", "path": "/transport", "framing": "webtransport"},
101
157
  {"kind": "webtransport", "path": "/transport", "exchange": "request_response"},
102
158
  ),
103
159
  )