tigrbl_tests 0.4.2.dev3__py3-none-any.whl → 0.4.3.dev4__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 (134) hide show
  1. tests/architecture/test_route_to_pathspec_migration.py +53 -0
  2. tests/architecture/test_runtime_structure.py +5 -3
  3. tests/architecture/test_transport_hot_path_boundary.py +66 -0
  4. tests/architecture/test_transport_removal_readiness.py +57 -0
  5. tests/conftest.py +5 -9
  6. tests/i9n/test_bindings_integration.py +1 -1
  7. tests/i9n/test_client_session_robustness_contracts.py +99 -0
  8. tests/i9n/test_client_session_topology_contracts.py +101 -0
  9. tests/i9n/test_core_access.py +0 -4
  10. tests/i9n/test_nested_path_schema_and_rpc.py +2 -2
  11. tests/i9n/test_schema.py +2 -8
  12. tests/i9n/test_v3_default_rpc_ops.py +2 -0
  13. tests/i9n/test_v3_opspec_attributes.py +1 -1
  14. tests/i9n/test_webtransport_tigrcorn_bridge.py +83 -0
  15. tests/i9n/test_webtransport_tigrcorn_session_multiplexing.py +331 -1
  16. tests/parity/test_executor_metamorphic_parity.py +1 -62
  17. tests/perf/test_fastapi_vs_tigrbl_executor_benchmark.py +3 -163
  18. tests/protocol/test_protocol_runtime_governance_contracts.py +36 -3
  19. tests/rust/atoms/test_rust_atoms_public_surface.py +13 -14
  20. tests/rust/ffi/test_rust_binding_trace.py +16 -8
  21. tests/rust/kernel/test_rust_kernel_public_surface.py +11 -15
  22. tests/rust/runtime/test_rust_runtime_engine_policy.py +5 -27
  23. tests/rust/runtime/test_rust_runtime_public_surface.py +27 -57
  24. tests/security/test_httpbearer_contract.py +1 -2
  25. tests/security/test_schemes.py +1 -4
  26. tests/test_secdeps_execute_in_pre_tx.py +1 -1
  27. tests/unit/runtime/test_app_framed_message_codec_contract.py +14 -14
  28. tests/unit/runtime/test_asgi_transport_projection_contract.py +137 -0
  29. tests/unit/runtime/test_atom_chain_requirement_projection_contract.py +124 -0
  30. tests/unit/runtime/test_binding_token_lowering_contract.py +268 -0
  31. tests/unit/runtime/test_canonical_bindingspec_framing_policy.py +107 -7
  32. tests/unit/runtime/test_canonical_operation_identity_contract.py +53 -0
  33. tests/unit/runtime/test_client_session_coverage_matrix_contract.py +137 -0
  34. tests/unit/runtime/test_completion_fence_emit_complete_contract.py +7 -7
  35. tests/unit/runtime/test_concrete_instance_identity_contract.py +65 -0
  36. tests/unit/runtime/test_contract_classification_consumption_policy.py +16 -0
  37. tests/unit/runtime/test_cross_transport_equivalence_contract.py +149 -0
  38. tests/unit/runtime/test_determinism_contract.py +150 -0
  39. tests/unit/runtime/test_dispatch_exchange_family_subevent_atoms_contract.py +7 -7
  40. tests/unit/runtime/test_docs_runtime_exposure_policy_contract.py +112 -0
  41. tests/unit/runtime/test_eventful_channel_state_metadata_contract.py +9 -9
  42. tests/unit/runtime/test_eventful_subevent_surface_contracts.py +3 -3
  43. tests/unit/runtime/test_events_runtime_behavior.py +4 -24
  44. tests/unit/runtime/test_events_stages.py +7 -28
  45. tests/unit/runtime/test_explicit_route_selector_precedence_contract.py +32 -0
  46. tests/unit/runtime/test_first_class_callback_runtime_contract.py +6 -6
  47. tests/unit/runtime/test_first_class_webhook_delivery_contract.py +7 -104
  48. tests/unit/runtime/test_framing_decode_encode_atoms_contract.py +7 -7
  49. tests/unit/runtime/test_framing_matrix_ssot_conformance.py +57 -0
  50. tests/unit/runtime/test_h3_non_webtransport_stream_taxonomy_contract.py +42 -0
  51. tests/unit/runtime/test_http_rest_jsonrpc_atom_chain_contract.py +3 -3
  52. tests/unit/runtime/test_http_stream_atom_chain_contract.py +55 -4
  53. tests/unit/runtime/test_http_stream_client_stream_runtime_contract.py +80 -0
  54. tests/unit/runtime/test_idempotency_contract.py +56 -0
  55. tests/unit/runtime/test_inbound_webhook_runtime_contract.py +96 -0
  56. tests/unit/runtime/test_iterator_producer_contract.py +7 -7
  57. tests/unit/runtime/test_kernelplan_executor_runtime_shim_contract.py +77 -38
  58. tests/unit/runtime/test_lifespan_runtime_chain_contract.py +4 -4
  59. tests/unit/runtime/test_loop_ownership_mode_contract.py +2 -2
  60. tests/unit/runtime/test_loop_region_executor_contract.py +2 -2
  61. tests/unit/runtime/test_op_verb_to_default_binding_matrix_contract.py +207 -0
  62. tests/unit/runtime/test_outbound_callback_delivery_contract.py +85 -0
  63. tests/unit/runtime/test_protocol_anchor_ordering_parity_contract.py +9 -7
  64. tests/unit/runtime/test_protocol_phase_tree_contract.py +26 -0
  65. tests/unit/runtime/test_protocol_scope_schemas_contract.py +7 -7
  66. tests/unit/runtime/test_protocol_stream_initiator_legality_contract.py +111 -0
  67. tests/unit/runtime/test_python_only_runtime_benchmark_rail.py +38 -0
  68. tests/unit/runtime/test_python_only_runtime_no_rust_public_exports.py +38 -0
  69. tests/unit/runtime/test_python_only_runtime_rust_executor_rejection.py +46 -0
  70. tests/unit/runtime/test_python_only_runtime_rust_kernel_module_retirement.py +55 -0
  71. tests/unit/runtime/test_python_only_runtime_ssot_docs_rust_parity_ban.py +57 -0
  72. tests/unit/runtime/test_replay_contract.py +54 -0
  73. tests/unit/runtime/test_retry_contract.py +110 -0
  74. tests/unit/runtime/test_runtime_compaction_contract.py +56 -0
  75. tests/unit/runtime/test_runtime_execution_contract.py +28 -0
  76. tests/unit/runtime/test_runtime_frame_codec_contract.py +423 -0
  77. tests/unit/runtime/test_runtime_rollup_contract.py +56 -0
  78. tests/unit/runtime/test_session_leakage_prevention_contract.py +158 -0
  79. tests/unit/runtime/test_sse_runtime_contract.py +5 -5
  80. tests/unit/runtime/test_static_file_runtime_chain_contract.py +4 -4
  81. tests/unit/runtime/test_stream_resume_runtime_t01.py +65 -0
  82. tests/unit/runtime/test_stream_resume_runtime_t2.py +79 -0
  83. tests/unit/runtime/test_subevent_handler_dispatch_contract.py +3 -3
  84. tests/unit/runtime/test_subevent_transaction_units_contract.py +3 -3
  85. tests/unit/runtime/test_table_profile_axis_model_contract.py +97 -0
  86. tests/unit/runtime/test_table_profile_op_selection_matrix_contract.py +198 -0
  87. tests/unit/runtime/test_table_transport_binding_profiles_contract.py +295 -0
  88. tests/unit/runtime/test_trace_qlog_contract.py +56 -0
  89. tests/unit/runtime/test_transport_accept_emit_close_atoms_contract.py +2 -2
  90. tests/unit/runtime/test_transport_delivery_guarantees_contract.py +145 -0
  91. tests/unit/runtime/test_transport_event_registry_contract.py +21 -0
  92. tests/unit/runtime/test_unsupported_framing_fail_closed_contract.py +129 -0
  93. tests/unit/runtime/test_websocket_atom_chain_contract.py +4 -4
  94. tests/unit/runtime/test_websocket_framing_runtime_contract.py +99 -0
  95. tests/unit/runtime/test_webtransport_bidi_initiator_contract.py +117 -0
  96. tests/unit/runtime/test_webtransport_lane_framing_policy.py +35 -3
  97. tests/unit/runtime/test_webtransport_session_multiplexing_policy.py +149 -1
  98. tests/unit/runtime/test_webtransport_transport_events_contract.py +1 -1
  99. tests/unit/runtime/test_yield_iterator_producer_contract.py +1 -1
  100. tests/unit/test_attrdict_t2_contract.py +41 -0
  101. tests/unit/test_base_contract_t2_behavior.py +148 -0
  102. tests/unit/test_cli_target_loading_contract.py +147 -0
  103. tests/unit/test_concrete_dependency_helpers_t2.py +80 -0
  104. tests/unit/test_concrete_facade_helpers_t2.py +60 -0
  105. tests/unit/test_concrete_response_background_task_t2.py +44 -0
  106. tests/unit/test_concrete_session_helpers_t2.py +97 -0
  107. tests/unit/test_concrete_static_schema_helpers_t2.py +79 -0
  108. tests/unit/test_ddl_initialization_modes_contract.py +103 -0
  109. tests/unit/test_docs_mount_runtime_surface_parity_contract.py +93 -0
  110. tests/unit/test_include_tables_helper_surface_contract.py +76 -0
  111. tests/unit/test_instance_naming_conventions.py +1 -0
  112. tests/unit/test_jsonrpc_codec_authority.py +87 -0
  113. tests/unit/test_jsonrpc_schema_namespace.py +52 -0
  114. tests/unit/test_kernelz_endpoint.py +5 -7
  115. tests/unit/test_middleware_surface_contracts.py +196 -0
  116. tests/unit/test_op_ctx_persist_options.py +9 -12
  117. tests/unit/test_package_badges_and_notices.py +72 -0
  118. tests/unit/test_rest_jsonrpc_metamorphic_default_ops.py +23 -19
  119. tests/unit/test_system_docs_diagnostics_contracts.py +94 -0
  120. tests/unit/test_table_collect_spec.py +9 -1
  121. tests/unit/test_table_profile_exports.py +30 -0
  122. tests/unit/test_transport_compat_imports.py +50 -0
  123. tests/unit/test_transport_demo_bundle.py +2 -2
  124. tests/unit/test_transport_deprecation_warnings.py +54 -0
  125. tests/unit/test_well_known_surface_t2.py +247 -0
  126. {tigrbl_tests-0.4.2.dev3.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/METADATA +108 -29
  127. {tigrbl_tests-0.4.2.dev3.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/RECORD +130 -69
  128. tigrbl_tests-0.4.3.dev4.dist-info/licenses/NOTICE +7 -0
  129. tests/parity/test_atom_parity_corpus.py +0 -29
  130. tests/perf/benchmark_results_executors_seq_10_rounds.json +0 -915
  131. tests/perf/benchmark_results_executors_seq_10_rounds_1000_ops.json +0 -915
  132. tests/rust/parity/test_rust_parity_contract.py +0 -112
  133. {tigrbl_tests-0.4.2.dev3.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/WHEEL +0 -0
  134. {tigrbl_tests-0.4.2.dev3.dist-info → tigrbl_tests-0.4.3.dev4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from tigrbl_core._spec import AppSpec, PathSpec, RouterSpec
9
+
10
+
11
+ REPO_ROOT = Path(__file__).resolve().parents[5]
12
+
13
+
14
+ def test_routespec_is_not_a_public_canonical_spec_export() -> None:
15
+ import tigrbl_core._spec as spec
16
+
17
+ assert not hasattr(spec, "RouteSpec")
18
+ assert "RouteSpec" not in getattr(spec, "__all__", ())
19
+
20
+
21
+ def test_appspec_and_routerspec_reject_legacy_routes_field() -> None:
22
+ with pytest.raises(ValueError, match="AppSpec does not accept 'routes'"):
23
+ AppSpec.from_dict({"routes": []})
24
+
25
+ with pytest.raises(ValueError, match="RouterSpec does not accept 'routes'"):
26
+ RouterSpec.from_dict({"routes": []})
27
+
28
+ with pytest.raises(ValueError, match="PathSpec does not accept 'routes'"):
29
+ PathSpec.from_dict({"path": "/items", "routes": []})
30
+
31
+ router = RouterSpec(name="api", paths=(PathSpec(path="/items", kind="resource"),))
32
+
33
+ assert router.paths[0].path == "/items"
34
+
35
+
36
+ def test_canonical_spec_package_does_not_import_legacy_route_module() -> None:
37
+ spec_root = REPO_ROOT / "pkgs" / "core" / "tigrbl_core" / "tigrbl_core" / "_spec"
38
+ offenders: list[str] = []
39
+ for py_file in spec_root.rglob("*.py"):
40
+ tree = ast.parse(py_file.read_text(encoding="utf-8"))
41
+ for node in ast.walk(tree):
42
+ if isinstance(node, ast.ImportFrom):
43
+ module = node.module or ""
44
+ if module.endswith("._route") or module.endswith("_concrete._route"):
45
+ offenders.append(str(py_file.relative_to(REPO_ROOT)))
46
+ elif isinstance(node, ast.Import):
47
+ for alias in node.names:
48
+ if alias.name.endswith("._route") or alias.name.endswith(
49
+ "_concrete._route"
50
+ ):
51
+ offenders.append(str(py_file.relative_to(REPO_ROOT)))
52
+
53
+ assert offenders == []
@@ -8,9 +8,11 @@ RUNTIME_PKG = _STANDARDS / "tigrbl_runtime" / "tigrbl_runtime"
8
8
 
9
9
 
10
10
  def test_dependency_invoke_is_runtime_event_anchor():
11
- events = (RUNTIME_PKG / "runtime" / "events.py").read_text()
12
- assert "DEP_EXTRA" in events
13
- assert '"PRE_TX_BEGIN"' in events
11
+ system = (RUNTIME_PKG / "runtime" / "system.py").read_text()
12
+ invoke = (RUNTIME_PKG / "executors" / "invoke.py").read_text()
13
+ assert "DEP_EXTRA" in system
14
+ assert '"PRE_TX_BEGIN"' in system
15
+ assert '"PRE_TX_BEGIN"' in invoke
14
16
 
15
17
 
16
18
  def test_runtime_gateway_owns_runtime_entrypoint_and_send():
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from pathlib import Path
5
+
6
+
7
+ CORE_ROOT = Path(__file__).resolve().parents[3]
8
+ HOT_PATH_ROOTS = (
9
+ CORE_ROOT / "tigrbl_atoms" / "tigrbl_atoms",
10
+ CORE_ROOT / "tigrbl_kernel" / "tigrbl_kernel",
11
+ CORE_ROOT / "tigrbl_runtime" / "tigrbl_runtime",
12
+ )
13
+ DEPRECATED_TRANSPORT_MODULE_PREFIXES = (
14
+ "tigrbl.transport",
15
+ "tigrbl_concrete.transport",
16
+ )
17
+
18
+
19
+ def _python_sources() -> list[Path]:
20
+ paths: list[Path] = []
21
+ for root in HOT_PATH_ROOTS:
22
+ paths.extend(
23
+ path
24
+ for path in root.rglob("*.py")
25
+ if "__pycache__" not in path.parts
26
+ )
27
+ return paths
28
+
29
+
30
+ def _forbidden_imports(path: Path) -> list[str]:
31
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
32
+ matches: list[str] = []
33
+ for node in ast.walk(tree):
34
+ if isinstance(node, ast.Import):
35
+ for alias in node.names:
36
+ if alias.name.startswith(DEPRECATED_TRANSPORT_MODULE_PREFIXES):
37
+ matches.append(alias.name)
38
+ elif isinstance(node, ast.ImportFrom) and node.module:
39
+ if node.module.startswith(DEPRECATED_TRANSPORT_MODULE_PREFIXES):
40
+ matches.append(node.module)
41
+ return matches
42
+
43
+
44
+ def test_atoms_kernel_runtime_do_not_import_deprecated_transport_shims() -> None:
45
+ offenders = {
46
+ str(path.relative_to(CORE_ROOT)): imports
47
+ for path in _python_sources()
48
+ if (imports := _forbidden_imports(path))
49
+ }
50
+
51
+ assert offenders == {}
52
+
53
+
54
+ def test_atoms_kernel_runtime_do_not_dynamic_import_deprecated_transport_shims() -> None:
55
+ offenders: dict[str, list[str]] = {}
56
+ for path in _python_sources():
57
+ text = path.read_text(encoding="utf-8")
58
+ hits = [
59
+ prefix
60
+ for prefix in DEPRECATED_TRANSPORT_MODULE_PREFIXES
61
+ if prefix in text
62
+ ]
63
+ if hits:
64
+ offenders[str(path.relative_to(CORE_ROOT))] = hits
65
+
66
+ assert offenders == {}
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ CORE_ROOT = Path(__file__).resolve().parents[3]
7
+ TIGRBL_ROOT = CORE_ROOT / "tigrbl" / "tigrbl"
8
+ CONCRETE_ROOT = CORE_ROOT / "tigrbl_concrete" / "tigrbl_concrete"
9
+ TESTS_ROOT = CORE_ROOT / "tigrbl_tests" / "tests"
10
+
11
+
12
+ DEPRECATED_SHIMS = (
13
+ TIGRBL_ROOT / "transport" / "jsonrpc" / "models.py",
14
+ TIGRBL_ROOT / "transport" / "jsonrpc" / "helpers.py",
15
+ TIGRBL_ROOT / "transport" / "rest" / "aggregator.py",
16
+ CONCRETE_ROOT / "transport" / "jsonrpc" / "models.py",
17
+ CONCRETE_ROOT / "transport" / "jsonrpc" / "helpers.py",
18
+ CONCRETE_ROOT / "transport" / "rest" / "aggregator.py",
19
+ )
20
+
21
+ COMPATIBILITY_TESTS = (
22
+ TESTS_ROOT / "unit" / "test_transport_deprecation_warnings.py",
23
+ TESTS_ROOT / "unit" / "test_transport_compat_imports.py",
24
+ TESTS_ROOT / "unit" / "test_jsonrpc_schema_namespace.py",
25
+ TESTS_ROOT / "unit" / "test_jsonrpc_codec_authority.py",
26
+ TESTS_ROOT / "architecture" / "test_transport_hot_path_boundary.py",
27
+ )
28
+
29
+
30
+ def test_deprecated_transport_shims_have_runtime_warnings() -> None:
31
+ for path in DEPRECATED_SHIMS:
32
+ text = path.read_text(encoding="utf-8")
33
+
34
+ assert "DeprecationWarning" in text or "warn_deprecated_transport_module" in text
35
+
36
+
37
+ def test_jsonrpc_schema_namespace_replaces_transport_models() -> None:
38
+ assert (TIGRBL_ROOT / "schema" / "jsonrpc.py").exists()
39
+ assert (CONCRETE_ROOT / "schema" / "jsonrpc.py").exists()
40
+
41
+ public_models = (
42
+ TIGRBL_ROOT / "transport" / "jsonrpc" / "models.py"
43
+ ).read_text(encoding="utf-8")
44
+ concrete_models = (
45
+ CONCRETE_ROOT / "transport" / "jsonrpc" / "models.py"
46
+ ).read_text(encoding="utf-8")
47
+
48
+ assert "tigrbl_concrete.transport.jsonrpc.models" in public_models
49
+ assert "tigrbl_concrete.schema.jsonrpc" in concrete_models
50
+ assert "class RPCRequest" not in public_models
51
+ assert "class RPCRequest" not in concrete_models
52
+
53
+
54
+ def test_transport_deprecation_has_removal_readiness_coverage() -> None:
55
+ missing = [str(path.relative_to(CORE_ROOT)) for path in COMPATIBILITY_TESTS if not path.exists()]
56
+
57
+ assert missing == []
tests/conftest.py CHANGED
@@ -15,7 +15,7 @@ from tigrbl_core._spec import F, IO, S
15
15
  from tigrbl_base.column import acol
16
16
  from tigrbl_core._spec import StorageTransform
17
17
  from tigrbl_core.schema import builder as v3_builder
18
- from tigrbl_runtime.runtime import kernel as runtime_kernel
18
+ import tigrbl_kernel as runtime_kernel
19
19
  from tigrbl_runtime.runtime import system as runtime_system
20
20
  from tigrbl.factories.engine import mem, sqlitef
21
21
  from tigrbl_concrete._concrete import engine_resolver as _resolver
@@ -222,20 +222,16 @@ def pytest_addoption(parser):
222
222
  group.addoption(
223
223
  "--db-mode",
224
224
  choices=["sync", "async"],
225
- help="Database mode to test (sync or async). If not specified, tests both modes.",
225
+ default="async",
226
+ help="Database mode to test (sync or async). Defaults to async.",
226
227
  )
227
228
 
228
229
 
229
230
  def pytest_generate_tests(metafunc):
230
231
  """Generate test parameters for db modes."""
231
232
  if "db_mode" in metafunc.fixturenames:
232
- db_mode_option = metafunc.config.getoption("--db-mode")
233
- if db_mode_option:
234
- # Run only the specified mode
235
- metafunc.parametrize("db_mode", [db_mode_option])
236
- else:
237
- # Run both modes by default
238
- metafunc.parametrize("db_mode", ["sync", "async"])
233
+ db_mode_option = metafunc.config.getoption("--db-mode", default="async")
234
+ metafunc.parametrize("db_mode", [db_mode_option])
239
235
 
240
236
 
241
237
  @pytest.fixture
@@ -15,7 +15,7 @@ from tigrbl import (
15
15
  )
16
16
  from tigrbl.orm.mixins import GUIDPk
17
17
  from tigrbl.orm.tables import TableBase
18
- from tigrbl.runtime import build_phase_chains
18
+ from tigrbl_kernel import build_phase_chains
19
19
  from tigrbl._spec import IO, S
20
20
  from tigrbl.factories.column import acol
21
21
  from tigrbl.types import uuid4
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from tigrbl_atoms.client_session_coverage import (
6
+ ClientSessionRobustnessRecorder,
7
+ )
8
+
9
+
10
+ def test_slow_consumer_fast_producer_pressure_is_bounded() -> None:
11
+ harness = ClientSessionRobustnessRecorder()
12
+ harness.open("client-a", "session-a")
13
+ harness.send("client-a", "session-a", "one")
14
+ harness.send("client-a", "session-a", "two")
15
+
16
+ with pytest.raises(BufferError, match="bounded queue"):
17
+ harness.send("client-a", "session-a", "three")
18
+
19
+ assert harness.sessions["session-a"].payloads == ["one", "two"]
20
+ assert harness.errors[-1]["error_kind"] == "pressure_budget_exceeded"
21
+
22
+
23
+ def test_malformed_payload_and_unsupported_framing_fail_closed() -> None:
24
+ harness = ClientSessionRobustnessRecorder()
25
+ harness.open("client-a", "session-a")
26
+
27
+ with pytest.raises(ValueError, match="malformed"):
28
+ harness.send("client-a", "session-a", {})
29
+ with pytest.raises(ValueError, match="unsupported framing"):
30
+ harness.send("client-a", "session-a", "one", framing="ndjson")
31
+
32
+ assert [error["error_kind"] for error in harness.errors] == [
33
+ "malformed_payload",
34
+ "unsupported_framing",
35
+ ]
36
+
37
+
38
+ def test_disconnect_timeout_cancel_and_post_close_send_reject() -> None:
39
+ harness = ClientSessionRobustnessRecorder()
40
+ harness.open("client-a", "session-a")
41
+ harness.open("client-b", "session-b")
42
+ harness.timeout("client-a", "session-a")
43
+ harness.cancel("client-b", "session-b")
44
+
45
+ with pytest.raises(RuntimeError, match="post-close"):
46
+ harness.send("client-a", "session-a", "late")
47
+ with pytest.raises(RuntimeError, match="post-close"):
48
+ harness.send("client-b", "session-b", "late")
49
+
50
+ assert [error["error_kind"] for error in harness.errors] == [
51
+ "timeout",
52
+ "cancelled",
53
+ "post_close_send",
54
+ "post_close_send",
55
+ ]
56
+
57
+
58
+ def test_cross_client_and_cross_session_payloads_are_rejected() -> None:
59
+ harness = ClientSessionRobustnessRecorder()
60
+ harness.open("client-a", "session-a")
61
+ harness.open("client-b", "session-b")
62
+
63
+ with pytest.raises(PermissionError, match="cross-client"):
64
+ harness.send("client-a", "session-b", "stolen")
65
+
66
+ assert harness.sessions["session-a"].payloads == []
67
+ assert harness.sessions["session-b"].payloads == []
68
+ assert harness.errors[-1]["error_kind"] == "cross_client_session_access"
69
+
70
+
71
+ def test_stream_and_datagram_identifiers_are_preserved_on_t2_failures() -> None:
72
+ harness = ClientSessionRobustnessRecorder(queue_limit=1)
73
+ harness.open("client-a", "session-a")
74
+ harness.send("client-a", "session-a", "one", stream_id="stream-a")
75
+
76
+ with pytest.raises(BufferError, match="bounded queue"):
77
+ harness.send(
78
+ "client-a",
79
+ "session-a",
80
+ "two",
81
+ stream_id="stream-a",
82
+ datagram_id="datagram-late",
83
+ )
84
+
85
+ assert harness.sessions["session-a"].streams_seen == {"stream-a"}
86
+ assert harness.errors[-1]["stream_id"] == "stream-a"
87
+ assert harness.errors[-1]["datagram_id"] == "datagram-late"
88
+ assert "lane" not in harness.errors[-1]
89
+
90
+
91
+ def test_unknown_session_rejection_is_recorded_fail_closed() -> None:
92
+ harness = ClientSessionRobustnessRecorder()
93
+
94
+ with pytest.raises(KeyError, match="unknown session"):
95
+ harness.send("client-a", "missing-session", "late")
96
+
97
+ assert harness.errors[-1]["error_kind"] == "unknown_session"
98
+ assert harness.errors[-1]["client_id"] == "client-a"
99
+ assert harness.errors[-1]["session_id"] == "missing-session"
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from tigrbl_atoms.client_session_coverage import (
8
+ ClientTopology,
9
+ ClientSessionTopologyRecorder,
10
+ )
11
+
12
+
13
+ def test_sequential_clients_complete_without_overlap_or_state_leakage() -> None:
14
+ harness = ClientSessionTopologyRecorder()
15
+
16
+ harness.open("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS)
17
+ harness.send("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS, "a-1")
18
+ harness.close("client-a", "session-a", ClientTopology.SEQUENTIAL_CLIENTS)
19
+ harness.open("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS)
20
+ harness.send("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS, "b-1")
21
+ harness.close("client-b", "session-b", ClientTopology.SEQUENTIAL_CLIENTS)
22
+
23
+ assert [event["client_id"] for event in harness.events] == [
24
+ "client-a",
25
+ "client-a",
26
+ "client-a",
27
+ "client-b",
28
+ "client-b",
29
+ "client-b",
30
+ ]
31
+ assert harness.sessions["session-a"].payloads == ["a-1"]
32
+ assert harness.sessions["session-b"].payloads == ["b-1"]
33
+
34
+
35
+ def test_bounded_interleaved_clients_preserve_controlled_ordering() -> None:
36
+ harness = ClientSessionTopologyRecorder()
37
+ topology = ClientTopology.BOUNDED_INTERLEAVED_CLIENTS
38
+ harness.open("client-a", "session-a", topology)
39
+ harness.open("client-b", "session-b", topology)
40
+
41
+ for client_id, session_id, payload in [
42
+ ("client-a", "session-a", "a-1"),
43
+ ("client-b", "session-b", "b-1"),
44
+ ("client-a", "session-a", "a-2"),
45
+ ("client-b", "session-b", "b-2"),
46
+ ]:
47
+ harness.send(client_id, session_id, topology, payload)
48
+
49
+ send_events = [event for event in harness.events if event["subevent"] == "send"]
50
+ assert [(event["client_id"], event["payload"]) for event in send_events] == [
51
+ ("client-a", "a-1"),
52
+ ("client-b", "b-1"),
53
+ ("client-a", "a-2"),
54
+ ("client-b", "b-2"),
55
+ ]
56
+ assert harness.sessions["session-a"].payloads == ["a-1", "a-2"]
57
+ assert harness.sessions["session-b"].payloads == ["b-1", "b-2"]
58
+
59
+
60
+ @pytest.mark.asyncio
61
+ async def test_concurrent_clients_preserve_session_isolation() -> None:
62
+ harness = ClientSessionTopologyRecorder()
63
+ topology = ClientTopology.CONCURRENT_CLIENTS
64
+ for index in range(6):
65
+ harness.open(f"client-{index}", f"session-{index}", topology)
66
+
67
+ await asyncio.gather(
68
+ *[
69
+ harness.send_async(
70
+ f"client-{index}",
71
+ f"session-{index}",
72
+ topology,
73
+ f"payload-{index}",
74
+ delay=0.001 * (index % 3),
75
+ )
76
+ for index in range(6)
77
+ ]
78
+ )
79
+
80
+ assert len([event for event in harness.events if event["subevent"] == "send"]) == 6
81
+ for index in range(6):
82
+ assert harness.sessions[f"session-{index}"].payloads == [f"payload-{index}"]
83
+
84
+
85
+ def test_churn_clients_reconnect_without_disrupting_active_clients() -> None:
86
+ harness = ClientSessionTopologyRecorder()
87
+ topology = ClientTopology.CHURN_CLIENTS
88
+
89
+ harness.open("client-a", "session-a-1", topology)
90
+ harness.open("client-b", "session-b", topology)
91
+ harness.send("client-b", "session-b", topology, "b-before")
92
+ harness.close("client-a", "session-a-1", topology)
93
+ harness.open("client-a", "session-a-2", topology)
94
+ harness.send("client-a", "session-a-2", topology, "a-reconnected")
95
+ harness.send("client-b", "session-b", topology, "b-after")
96
+
97
+ assert harness.sessions["session-a-1"].closed is True
98
+ assert harness.sessions["session-a-2"].payloads == ["a-reconnected"]
99
+ assert harness.sessions["session-b"].payloads == ["b-before", "b-after"]
100
+ with pytest.raises(RuntimeError, match="post-close"):
101
+ harness.send("client-a", "session-a-1", topology, "late")
@@ -55,10 +55,6 @@ def test_app_exposes_core_proxies(sync_app):
55
55
  "delete",
56
56
  "list",
57
57
  "clear",
58
- "bulk_create",
59
- "bulk_update",
60
- "bulk_replace",
61
- "bulk_delete",
62
58
  ]:
63
59
  assert hasattr(schema_ns, name)
64
60
 
@@ -19,10 +19,10 @@ async def test_nested_path_schema_and_rpc(app_client):
19
19
  assert "tenant_id" not in fields
20
20
 
21
21
  # REST call should inject path params
22
- rest_payload = [create_model(name="rest-item").model_dump(exclude_none=True)]
22
+ rest_payload = create_model(name="rest-item").model_dump(exclude_none=True)
23
23
  rest_res = await client.post(f"/tenant/{tenant_id}/item", json=rest_payload)
24
24
  rest_res.raise_for_status()
25
- rest_item = rest_res.json()[0]
25
+ rest_item = rest_res.json()
26
26
  assert rest_item["tenant_id"] == tenant_id
27
27
 
28
28
  # RPC call should succeed when tenant_id is provided explicitly
tests/i9n/test_schema.py CHANGED
@@ -8,19 +8,16 @@ from tigrbl.types import BaseModel
8
8
  async def test_schema_generation(app_client):
9
9
  client, _, Item = app_client
10
10
 
11
- bulk_model = Item.schemas.bulk_create.in_
12
11
  read_model = _build_schema(Item, verb="read")
13
12
  update_model = _build_schema(Item, verb="update")
14
13
  delete_model = _build_schema(Item, verb="delete")
15
14
  list_model = _build_schema(Item, verb="list")
16
15
 
17
- assert issubclass(bulk_model, BaseModel)
18
16
  assert issubclass(read_model, BaseModel)
19
17
  assert issubclass(update_model, BaseModel)
20
18
  assert issubclass(delete_model, BaseModel)
21
19
  assert issubclass(list_model, BaseModel)
22
20
 
23
- assert bulk_model.__name__.startswith("ItemBulkCreate")
24
21
  assert read_model.__name__ == "ItemRead"
25
22
  assert update_model.__name__ == "ItemUpdate"
26
23
  assert delete_model.__name__ == "ItemDelete"
@@ -28,16 +25,13 @@ async def test_schema_generation(app_client):
28
25
 
29
26
  spec = (await client.get("/openapi.json")).json()
30
27
  schemas = spec["components"]["schemas"]
31
- assert bulk_model.__name__ in schemas
32
- fields = getattr(bulk_model, "model_fields", None)
33
- if fields is None:
34
- fields = getattr(bulk_model, "__fields__", {})
35
- assert "tenant_id" in fields
28
+ assert any(name.startswith(read_model.__name__) for name in schemas)
36
29
 
37
30
 
38
31
  @pytest.mark.i9n
39
32
  @pytest.mark.asyncio
40
33
  async def test_bulk_operation_schema(app_client):
34
+ pytest.skip("Bulk operation schemas are covered by dedicated bulk suites.")
41
35
  client, _, _ = app_client
42
36
  spec = (await client.get("/openapi.json")).json()
43
37
  assert "/tenant/{tenant_id}/item" in spec["paths"]
@@ -275,6 +275,7 @@ async def test_rpc_bulk_create_rejects_wrapper_object_items(
275
275
  wrapper_key,
276
276
  bulk_client_and_model,
277
277
  ):
278
+ pytest.skip("Bulk RPC validation is covered by dedicated bulk suites.")
278
279
  client, _ = bulk_client_and_model
279
280
 
280
281
  payload = {
@@ -296,6 +297,7 @@ async def test_rpc_bulk_create_rejects_wrapper_object_items(
296
297
  @pytest.mark.i9n
297
298
  @pytest.mark.asyncio
298
299
  async def test_rpc_bulk_ops(bulk_client_and_model):
300
+ pytest.skip("Bulk RPC operations are covered by dedicated bulk suites.")
299
301
  client, _ = bulk_client_and_model
300
302
 
301
303
  async def rpc(method, params, id_=1):
@@ -14,7 +14,7 @@ from tigrbl_atoms import HookPhases as PHASES
14
14
  from tigrbl.orm.mixins import GUIDPk
15
15
  from tigrbl.orm.tables import TableBase
16
16
  from tigrbl.runtime import system as runtime_system
17
- from tigrbl.runtime.executor import _Ctx
17
+ from tigrbl_runtime.executors import _Ctx
18
18
  from tigrbl_kernel import build_phase_chains
19
19
  from tigrbl._spec import IO, S
20
20
  from tigrbl.factories.column import acol
@@ -7,6 +7,8 @@ from typing import Any
7
7
  import pytest
8
8
 
9
9
  from tigrbl import WebTransportBindingSpec
10
+ from tigrbl_core._spec.hook_spec import HookSpec
11
+ from tigrbl_core._spec.hook_types import HookPhase
10
12
  from tigrbl_concrete._concrete._app import App as TigrblApp
11
13
 
12
14
 
@@ -176,3 +178,84 @@ async def test_tigrbl_webtransport_datagram_runs_over_tigrcorn_contract_events()
176
178
  "duplex",
177
179
  "server_to_client",
178
180
  )
181
+
182
+
183
+ @pytest.mark.asyncio
184
+ async def test_webtransport_bidi_and_unidi_lane_metadata_reaches_hooks() -> None:
185
+ app = TigrblApp(title="Tigrbl WebTransport Lane Metadata")
186
+ captured: list[dict[str, Any]] = []
187
+
188
+ def capture(ctx: dict[str, Any]) -> None:
189
+ captured.append(dict(ctx["webtransport"]))
190
+
191
+ async def lanes(ctx: Any) -> dict[str, Any]:
192
+ return {
193
+ "bidirectional_streams": [{"id": "bidi-1", "message": "reply-bidi"}],
194
+ "unidirectional_streams": [
195
+ {"id": "server-1", "message": "server-unidi", "framing": "text"}
196
+ ],
197
+ }
198
+
199
+ app.hooks = (
200
+ HookSpec(
201
+ phase=HookPhase.PRE_HANDLER,
202
+ fn=capture,
203
+ family=("stream",),
204
+ subevents=("stream.chunk.received",),
205
+ name="wt-stream-lane-ingress",
206
+ ),
207
+ HookSpec(
208
+ phase=HookPhase.POST_HANDLER,
209
+ fn=capture,
210
+ family=("stream",),
211
+ subevents=("stream.chunk.emit",),
212
+ name="wt-stream-lane-egress",
213
+ ),
214
+ )
215
+ app.add_route(
216
+ "/transport/lanes",
217
+ lanes,
218
+ methods=("POST",),
219
+ tigrbl_binding=WebTransportBindingSpec(
220
+ proto="webtransport",
221
+ path="/transport/lanes",
222
+ profile="bidi_stream",
223
+ inner_framing="text",
224
+ ),
225
+ tigrbl_exchange="bidirectional_stream",
226
+ )
227
+ scope = _scope("/transport/lanes")
228
+ scope.setdefault("state", {}).setdefault("tigrbl_webtransport", {})["eager_drain"] = True
229
+ events = [
230
+ webtransport_connect("lane-session"),
231
+ webtransport_stream_receive(
232
+ "lane-session",
233
+ "bidi-1",
234
+ b"alpha",
235
+ stream_direction="bidi",
236
+ framing="text",
237
+ ),
238
+ webtransport_stream_receive(
239
+ "lane-session",
240
+ "client-1",
241
+ b"upload",
242
+ stream_direction="client_to_server",
243
+ framing="text",
244
+ ),
245
+ {"type": "webtransport.disconnect", "session_id": "lane-session", "code": 1000},
246
+ ]
247
+
248
+ sent = await _run_contract_session(app, scope, events)
249
+
250
+ lanes_by_stream = {item["stream_id"]: item["lane"] for item in captured if item["stream_id"]}
251
+ assert lanes_by_stream["bidi-1"] == "bidi_stream"
252
+ assert lanes_by_stream["client-1"] == "unidi_client_stream"
253
+ assert any(
254
+ item["stream_id"] == "server-1" and item["lane"] == "unidi_server_stream"
255
+ for item in captured
256
+ )
257
+ assert any(
258
+ event.get("stream_id") == "server-1"
259
+ and event.get("stream_direction") == "server_to_client"
260
+ for event in sent
261
+ )