@githolon/dsl 0.108.6 → 0.110.0

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 (307) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/check_package.mjs +12 -15
  3. package/compile_package.mjs +17 -18
  4. package/compile_proof.mjs +16 -1
  5. package/dart/CHANGELOG.md +76 -1
  6. package/dart/README.md +25 -2
  7. package/dart/lib/src/admission_decision.dart +41 -0
  8. package/dart/lib/src/application_framework_calls.dart +52 -2
  9. package/dart/lib/src/application_model_adoption.dart +187 -0
  10. package/dart/lib/src/application_status.dart +73 -0
  11. package/dart/lib/src/console_enrolment.dart +271 -0
  12. package/dart/lib/src/console_http_io.dart +26 -0
  13. package/dart/lib/src/console_http_stub.dart +7 -0
  14. package/dart/lib/src/dev_cloud.dart +69 -22
  15. package/dart/lib/src/generated_call.dart +83 -23
  16. package/dart/lib/src/kernel_primitives.dart +119 -0
  17. package/dart/lib/src/nms_conformance_runtime.dart +1 -1
  18. package/dart/lib/src/nms_generated_runtime.dart +32 -10
  19. package/dart/lib/src/nms_test_environment.dart +1 -1
  20. package/dart/lib/src/node_runner_transport.dart +41 -46
  21. package/dart/lib/src/observed_session_directory.dart +70 -0
  22. package/dart/lib/src/operation_receipts.dart +112 -0
  23. package/dart/lib/src/peer_application_streams.dart +103 -0
  24. package/dart/lib/src/reception/dart_reception.dart +4280 -0
  25. package/dart/lib/src/reception/event_desk.dart +211 -0
  26. package/dart/lib/src/reception/extremum_read.dart +46 -0
  27. package/dart/lib/src/reception/local_copies.dart +371 -0
  28. package/dart/lib/src/reception/peer_office_connection.dart +11 -0
  29. package/dart/lib/src/reception/progress_desk.dart +267 -0
  30. package/dart/lib/src/reception/reception_contract.dart +11 -0
  31. package/dart/lib/src/reception/reception_session.dart +284 -0
  32. package/dart/lib/src/reception/sync_diagnostics.dart +26 -0
  33. package/dart/lib/src/reception/workspace_preparation_report.dart +126 -0
  34. package/dart/lib/src/replica_readiness.dart +123 -0
  35. package/dart/lib/src/runner/node-entry.mjs +23905 -15774
  36. package/dart/lib/src/runtime_v2_gateway.dart +56 -3
  37. package/dart/lib/src/runtime_v2_transport.dart +544 -3
  38. package/dart/lib/src/session_bound.dart +200 -311
  39. package/dart/lib/src/signing_identity_kernel.dart +84 -0
  40. package/dart/lib/src/sync_status_projection.dart +548 -0
  41. package/dart/lib/src/sync_status_reducer.dart +170 -0
  42. package/dart/lib/src/test_harness.dart +809 -1153
  43. package/dart/lib/src/testing/test_custody.dart +66 -0
  44. package/dart/lib/src/testing/test_peer.dart +328 -0
  45. package/dart/lib/src/typed_watch_registry.dart +169 -0
  46. package/dart/lib/src/workspace_open_progress.dart +27 -0
  47. package/dart/pubspec.lock +24 -1
  48. package/dart/pubspec.yaml +7 -2
  49. package/dart/pubspec_overrides.yaml +2 -0
  50. package/dart/test/admission_decision_test.dart +48 -0
  51. package/dart/test/background_model_adoption_test.dart +228 -0
  52. package/dart/test/bundle_version_pairing_test.dart +4 -4
  53. package/dart/test/child_authentication_repair_test.dart +359 -0
  54. package/dart/test/cloud_bound_local_shell_test.dart +119 -0
  55. package/dart/test/console_enrolment_refusal_test.dart +52 -0
  56. package/dart/test/console_enrolment_test.dart +227 -0
  57. package/dart/test/errors_test.dart +5 -5
  58. package/dart/test/event_plane_seam_test.dart +50 -164
  59. package/dart/test/extremum_read_test.dart +58 -0
  60. package/dart/test/fixtures/wedge/wedge-entry.mjs +3 -21
  61. package/dart/test/generated_query_test.dart +32 -0
  62. package/dart/test/harness_phase_timeout_test.dart +18 -23
  63. package/dart/test/kernel_author_after_read_test.dart +242 -0
  64. package/dart/test/kernel_fence_test.dart +137 -0
  65. package/dart/test/local_copies_test.dart +222 -0
  66. package/dart/test/model_adoption_test.dart +198 -0
  67. package/dart/test/node_peer_local_custody_test.dart +153 -0
  68. package/dart/test/offer_authentication_proof_test.dart +352 -52
  69. package/dart/test/operation_compatibility_test.dart +151 -0
  70. package/dart/test/platform_credential_effect_test.dart +386 -0
  71. package/dart/test/receiving_group_open_test.dart +213 -0
  72. package/dart/test/reception/workspace_preparation_report_test.dart +103 -0
  73. package/dart/test/reception_span_trace_test.dart +23 -0
  74. package/dart/test/replica_readiness_test.dart +156 -0
  75. package/dart/test/runtime_projections_test.dart +92 -0
  76. package/dart/test/runtime_v2_gateway_test.dart +131 -0
  77. package/dart/test/signing_identity_kernel_test.dart +75 -0
  78. package/dart/test/sync_status_preparation_report_test.dart +29 -0
  79. package/dart/test/sync_status_reducer_test.dart +419 -0
  80. package/dart/test/test_harness_test.dart +71 -95
  81. package/dart/test/typed_query_watch_test.dart +170 -0
  82. package/dart/test/typed_watch_registry_test.dart +249 -0
  83. package/dart_lints/README.md +18 -0
  84. package/dart_lints/lib/nomos_lints.dart +70 -0
  85. package/dart_lints/pubspec.lock +580 -0
  86. package/dart_lints/pubspec.yaml +12 -0
  87. package/dart_lints/test/reference_identity_test.dart +75 -0
  88. package/dart_types/CHANGELOG.md +18 -0
  89. package/dart_types/lib/nomos_kernel_protocol.dart +21 -1
  90. package/dart_types/lib/nomos_types.dart +9 -1
  91. package/dart_types/lib/src/admission_decision.dart +37 -0
  92. package/dart_types/lib/src/aggregate_meta.dart +1 -1
  93. package/dart_types/lib/src/approved_model.dart +65 -0
  94. package/dart_types/lib/src/custody.dart +10 -18
  95. package/dart_types/lib/src/error_info.dart +4 -4
  96. package/dart_types/lib/src/errors.dart +29 -29
  97. package/dart_types/lib/src/foreign_ref.dart +62 -33
  98. package/dart_types/lib/src/local_work_rejected.dart +3 -3
  99. package/dart_types/lib/src/nomos_ref.dart +3 -1
  100. package/dart_types/lib/src/principal.dart +2 -2
  101. package/dart_types/lib/src/proto/google/protobuf/any.pb.dart +163 -0
  102. package/dart_types/lib/src/proto/google/protobuf/any.pbenum.dart +11 -0
  103. package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pb.dart +137 -0
  104. package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pbenum.dart +11 -0
  105. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +20721 -10433
  106. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +555 -27
  107. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +387 -0
  108. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pbenum.dart +46 -0
  109. package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pb.dart +295 -0
  110. package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pbenum.dart +139 -0
  111. package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pb.dart +269 -0
  112. package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pbenum.dart +103 -0
  113. package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pb.dart +417 -0
  114. package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pbenum.dart +62 -0
  115. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +9968 -0
  116. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +133 -0
  117. package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pb.dart +204 -0
  118. package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pbenum.dart +54 -0
  119. package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pb.dart +1550 -0
  120. package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pbenum.dart +11 -0
  121. package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pb.dart +710 -0
  122. package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pbenum.dart +11 -0
  123. package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pb.dart +845 -0
  124. package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pbenum.dart +11 -0
  125. package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pb.dart +198 -0
  126. package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pbenum.dart +11 -0
  127. package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pb.dart +369 -0
  128. package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pbenum.dart +11 -0
  129. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +609 -0
  130. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pbenum.dart +65 -0
  131. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +358 -0
  132. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pbenum.dart +100 -0
  133. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +2231 -0
  134. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +271 -0
  135. package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pb.dart +1286 -0
  136. package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pbenum.dart +11 -0
  137. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +636 -0
  138. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pbenum.dart +116 -0
  139. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +1637 -0
  140. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pbenum.dart +175 -0
  141. package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pb.dart +1269 -0
  142. package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pbenum.dart +129 -0
  143. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +2782 -0
  144. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pbenum.dart +97 -0
  145. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +2232 -8627
  146. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +0 -358
  147. package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pb.dart +1639 -0
  148. package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pbenum.dart +46 -0
  149. package/dart_types/lib/src/runtime_outcome.dart +133 -0
  150. package/dart_types/lib/src/subscriptions.dart +116 -23
  151. package/dart_types/pubspec.yaml +1 -1
  152. package/dart_types/test/extremum_subscriptions_test.dart +40 -0
  153. package/dart_types/test/foreign_ref_resolution_test.dart +75 -0
  154. package/dart_types/test/nomos_ref_address_test.dart +25 -0
  155. package/dart_types/test/ordered_subscriptions_test.dart +34 -0
  156. package/operation_contract_check.mjs +24 -0
  157. package/package.json +11 -6
  158. package/src/aggregate.ts +70 -10
  159. package/src/application_topology.ts +18 -11
  160. package/src/attested.ts +3 -0
  161. package/src/attested_core.ts +46 -3
  162. package/src/birth_meter_ensure.ts +19 -7
  163. package/src/births.ts +29 -4
  164. package/src/build_package.ts +140 -13
  165. package/src/business_birth.ts +181 -4
  166. package/src/codegen_call_proto.ts +1274 -0
  167. package/src/codegen_call_usda.ts +49 -6
  168. package/src/codegen_dart.ts +79 -44
  169. package/src/codegen_dart_client.ts +458 -800
  170. package/src/{codegen_dart_application.ts → codegen_dart_facade.ts} +331 -582
  171. package/src/codegen_dart_framework_calls.ts +320 -37
  172. package/src/codegen_dart_indexed_variant.ts +159 -0
  173. package/src/codegen_dart_scenario.ts +42 -13
  174. package/src/codegen_proof.ts +3 -3
  175. package/src/codegen_provider_dart.ts +5 -0
  176. package/src/codegen_query_proto.ts +158 -0
  177. package/src/codegen_refusal.ts +189 -0
  178. package/src/codegen_result_proto.ts +931 -0
  179. package/src/codegen_result_usda.ts +4 -0
  180. package/src/codegen_ts.ts +464 -616
  181. package/src/codegen_workspace_access.ts +105 -0
  182. package/src/compatibility_graph.ts +759 -0
  183. package/src/compatibility_usda.ts +264 -0
  184. package/src/compile_package_main.ts +2083 -1248
  185. package/src/custody_placement.ts +156 -0
  186. package/src/custody_placement_usda.ts +91 -0
  187. package/src/decl_shapes.ts +21 -2
  188. package/src/describe_model.ts +1 -0
  189. package/src/directive.ts +59 -2
  190. package/src/drivers.ts +51 -0
  191. package/src/engine_entry.ts +52 -8
  192. package/src/evolve_preview.ts +11 -4
  193. package/src/exists.ts +2 -0
  194. package/src/fields.ts +75 -11
  195. package/src/flow_gate.ts +2 -1
  196. package/src/framework/application_home.ts +170 -18
  197. package/src/framework/attested_receipts.ts +8 -3
  198. package/src/framework/auth_provider_law.ts +169 -9
  199. package/src/framework/birth_certificate_law.ts +4 -0
  200. package/src/framework/capability.ts +34 -3
  201. package/src/framework/child_authentication.ts +346 -0
  202. package/src/framework/console_governance.ts +295 -0
  203. package/src/framework/delegated_roles.ts +301 -0
  204. package/src/framework/domain_lifecycle.ts +26 -13
  205. package/src/framework/founding_genesis.ts +193 -0
  206. package/src/framework/framework_aggregates.ts +1 -1
  207. package/src/framework/genesis.ts +6 -1
  208. package/src/framework/governance_relations.ts +19 -0
  209. package/src/framework/home.ts +5 -734
  210. package/src/framework/legacy/child_auth_provider_epoch.ts +73 -0
  211. package/src/framework/model_authority_repair.ts +116 -0
  212. package/src/framework/model_authority_seed.ts +185 -0
  213. package/src/framework/model_transition.ts +211 -0
  214. package/src/framework/owned_custody.ts +1 -1
  215. package/src/framework/principal_enrollment.ts +302 -0
  216. package/src/framework/receiving_group.ts +857 -0
  217. package/src/framework/receiving_group_recovery.ts +314 -0
  218. package/src/framework/relations.ts +4 -8
  219. package/src/framework/signing_identity.ts +242 -95
  220. package/src/framework/source_roster.ts +11 -5
  221. package/src/framework/workspace_access.ts +149 -0
  222. package/src/framework/workspace_invariant.ts +73 -52
  223. package/src/framework/workspaces.ts +407 -40
  224. package/src/framework_call_definitions.ts +32 -3
  225. package/src/generated_client_bundle_boundary.ts +97 -0
  226. package/src/generated_client_names.ts +1 -0
  227. package/src/identity_usda.ts +27 -0
  228. package/src/index.ts +183 -12
  229. package/src/indexed_variant.ts +83 -0
  230. package/src/intent.ts +173 -45
  231. package/src/law_check.ts +8 -5
  232. package/src/law_dependency_index.ts +231 -0
  233. package/src/law_graph.ts +508 -71
  234. package/src/law_graph_usda.ts +170 -24
  235. package/src/law_identity.ts +119 -81
  236. package/src/law_source_map.ts +4 -1
  237. package/src/law_units.ts +498 -0
  238. package/src/law_value_type.ts +7 -3
  239. package/src/member_authentication.ts +302 -0
  240. package/src/null_meta.ts +1 -1
  241. package/src/openusd_compiled_layer.ts +132 -0
  242. package/src/operation_contract_check.ts +31 -0
  243. package/src/ordered_read.ts +2 -1
  244. package/src/package_config.ts +33 -12
  245. package/src/package_dependencies.ts +1 -0
  246. package/src/plan_source_slice.ts +30 -1
  247. package/src/protobuf_field_number.ts +36 -0
  248. package/src/provenance_wire.ts +6 -0
  249. package/src/query.ts +2 -1
  250. package/src/read.ts +50 -0
  251. package/src/relation.ts +4 -1
  252. package/src/relation_schema.ts +15 -2
  253. package/src/report.ts +2 -2
  254. package/src/sqlite_computation_bundle.ts +78 -0
  255. package/src/sqlite_computed.ts +102 -0
  256. package/src/sqlite_counts.ts +154 -0
  257. package/src/sqlite_layout.ts +823 -0
  258. package/src/sqlite_reads.ts +275 -0
  259. package/src/sqlite_samples.ts +185 -0
  260. package/src/stable_id_identity_bridge.ts +1 -1
  261. package/src/stable_ids_for_module.ts +16 -4
  262. package/src/test_call_usda.ts +6 -6
  263. package/src/usda_relationship_integrity.ts +20 -3
  264. package/src/usda_text.ts +21 -1
  265. package/src/value_object.ts +81 -11
  266. package/src/wire.ts +22 -0
  267. package/src/wire_encode.ts +227 -20
  268. package/src/workspace_sharding.ts +15 -6
  269. package/src/workspace_sharing.ts +51 -49
  270. package/src/zod_introspection.ts +2 -0
  271. package/compat_package.mjs +0 -46
  272. package/dart/lib/src/home.dart +0 -15
  273. package/dart/lib/src/runner/offline-install.mjs +0 -15367
  274. package/dart/lib/src/source_trust.dart +0 -51
  275. package/dart/lib/src/webview_bridge.dart +0 -6540
  276. package/dart/lib/src/workspace_name.dart +0 -30
  277. package/dart/test/acquire_session_credentials_test.dart +0 -150
  278. package/dart/test/acquire_session_test.dart +0 -534
  279. package/dart/test/adv_realm_interference_test.dart +0 -600
  280. package/dart/test/cloud_bound_topology_test.dart +0 -168
  281. package/dart/test/cluster_births_test.dart +0 -202
  282. package/dart/test/cluster_device_keyed_acceptance_test.dart +0 -105
  283. package/dart/test/cluster_local_autosync_test.dart +0 -123
  284. package/dart/test/cluster_scale_test.dart +0 -122
  285. package/dart/test/compat_test.dart +0 -61
  286. package/dart/test/dlq_session_surface_test.dart +0 -222
  287. package/dart/test/first_app_gauntlet_test.dart +0 -386
  288. package/dart/test/fixtures/wedge/fake-install.mjs +0 -11
  289. package/dart/test/four_layer_nested_topology_test.dart +0 -290
  290. package/dart/test/home_parity_test.dart +0 -13
  291. package/dart/test/local_sync_inert_test.dart +0 -107
  292. package/dart/test/node_runner_transport_test.dart +0 -78
  293. package/dart/test/parent_signer_recovery_test.dart +0 -155
  294. package/dart/test/residency_refusal_test.dart +0 -49
  295. package/dart/test/sealing_client_test.dart +0 -181
  296. package/dart/test/session_bound_test.dart +0 -285
  297. package/dart/test/stale_bundle_test.dart +0 -80
  298. package/dart/test/vo_read_typing_test.dart +0 -131
  299. package/dart/test/watch_after_offer_stall_test.dart +0 -100
  300. package/dart/test/webview_bridge_test.dart +0 -1930
  301. package/dart/test/workspace_name_test.dart +0 -24
  302. package/runtime/generated-application-host.js +0 -22860
  303. package/src/codegen_query_usda.ts +0 -214
  304. package/src/compat_manifest.ts +0 -26
  305. package/src/compat_package_main.ts +0 -224
  306. package/src/shape_compat.ts +0 -352
  307. package/src/usd_state.ts +0 -505
@@ -0,0 +1,48 @@
1
+ import 'dart:convert';
2
+ import 'package:nomos_client/src/admission_decision.dart';
3
+ import 'package:nomos_client/src/reception/dart_reception.dart';
4
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
5
+ import 'package:test/test.dart';
6
+
7
+ p.OfferResponse refusal({bool domain = true}) => p.OfferResponse(refused: p.OfferRefused(
8
+ refusal: p.KernelRefusal(code: domain ? p.KernelRefusalCode.KERNEL_REFUSAL_CODE_DOMAIN_REFUSED
9
+ : p.KernelRefusalCode.KERNEL_REFUSAL_CODE_INVARIANT_EXECUTION_FAULT, message: 'test'),
10
+ domainRefusal: domain ? p.DomainRefusal(lawSha256: List.filled(32, 7), invariant: '/Rule',
11
+ typeIdentity: '/Problem', valueUsda: utf8.encode('#usda 1.0')) : null));
12
+ void main() {
13
+ test('preserves the successful business result', () async {
14
+ final result = await nmsAdmissionDecision(() async => 42, (_) => 'unused');
15
+ expect(result, isA<Admitted<int, String>>().having((value) => value.value, 'value', 42));
16
+ });
17
+ test('decodes declared refusal and copies its ephemeral evidence', () async {
18
+ final reply = refusal();
19
+ final result = await nmsAdmissionDecision<int, String>(() async => throw NomosTypedOfferFailure(reply), (_) => 'tooShort');
20
+ expect(result, isA<DomainRefused<int, String>>().having((value) => value.reason, 'reason', 'tooShort'));
21
+ reply.refused.domainRefusal.lawSha256[0] = 1;
22
+ final evidence = (result as DomainRefused).evidence;
23
+ expect(evidence.lawSha256[0], 7);
24
+ expect(() => evidence.lawSha256[0] = 0, throwsUnsupportedError);
25
+ });
26
+ test('framework rejection is separate from tenant refusal', () async {
27
+ final result = await nmsAdmissionDecision<int, String>(() async => throw NomosTypedOfferFailure(refusal(domain: false)), (_) => throw StateError('must not decode'));
28
+ expect(result, isA<FrameworkRefused<int, String>>());
29
+ });
30
+ test('unknown family and unknown arm preserve an explicit unsupported result', () async {
31
+ for (final decoder in <String? Function(NomosDomainRefusalEvidence)>[
32
+ (_) => null, (_) => throw const NmsUnsupportedNominalRefusal(),
33
+ ]) {
34
+ expect(await nmsAdmissionDecision<int, String>(() async => throw NomosTypedOfferFailure(refusal()), decoder), isA<UnsupportedRefusalContract<int, String>>());
35
+ }
36
+ });
37
+ test('malformed evidence and invalid codec output remain protocol faults', () async {
38
+ final bad = refusal()..refused.domainRefusal.lawSha256 = [1];
39
+ await expectLater(nmsAdmissionDecision<int, String>(() async => throw NomosTypedOfferFailure(bad), (_) => 'bad'), throwsFormatException);
40
+ await expectLater(nmsAdmissionDecision<int, String>(() async => throw NomosTypedOfferFailure(refusal()), (_) => throw const FormatException('bad body')), throwsFormatException);
41
+ });
42
+ test('transport and dependency failures are never disguised as business outcomes', () async {
43
+ final transport = StateError('offline');
44
+ await expectLater(nmsAdmissionDecision<int, String>(() async => throw transport, (_) => 'unused'), throwsA(same(transport)));
45
+ final blocked = NomosTypedOfferFailure(p.OfferResponse());
46
+ await expectLater(nmsAdmissionDecision<int, String>(() async => throw blocked, (_) => 'unused'), throwsA(same(blocked)));
47
+ });
48
+ }
@@ -0,0 +1,228 @@
1
+ import 'dart:async';
2
+ import 'dart:typed_data';
3
+
4
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
5
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
6
+ import 'package:test/test.dart';
7
+
8
+ // The real Reception, with delayed typed replies instead of a network. No
9
+ // lifecycle decisions belong here; the shared office tests cover those.
10
+ final class _Office
11
+ with NmsPlatformCredentialProviderHost
12
+ implements PeerOfficeConnection {
13
+ final calls = <p.RuntimeRequest>[];
14
+ final replies = <Completer<p.RuntimeResponse>>[];
15
+ int _sequence = 0;
16
+
17
+ @override
18
+ Stream<p.RuntimeEvent> get runtimeEvents => const Stream.empty();
19
+ @override
20
+ int nextRuntimeRequestId() => ++_sequence;
21
+
22
+ @override
23
+ Future<p.RuntimeResponse> invokeRuntime(p.RuntimeRequest request) {
24
+ if (request.hasCloseRealm()) {
25
+ return Future.value(p.RuntimeResponse(
26
+ protocolVersion: 2,
27
+ requestId: request.requestId,
28
+ realmClosed: p.RealmClosed()));
29
+ }
30
+ expect(request.hasAdoptApplicationModel(), isTrue);
31
+ calls.add(request.deepCopy());
32
+ final reply = Completer<p.RuntimeResponse>();
33
+ replies.add(reply);
34
+ return reply.future;
35
+ }
36
+
37
+ void finish(int index) => replies[index].complete(p.RuntimeResponse(
38
+ protocolVersion: 2,
39
+ requestId: calls[index].requestId,
40
+ applicationModelAdopted: p.ApplicationModelAdopted(
41
+ workspace: calls[index].adoptApplicationModel.session,
42
+ outcome: p.ModelAdoptionOutcome.MODEL_ADOPTION_OUTCOME_CURRENT)));
43
+ }
44
+
45
+ // A Reception whose office answers session binding as well, so a real
46
+ // `bindWorkspace` — the door the generated client opens a child through —
47
+ // can be driven end to end.
48
+ final class _BindingOffice
49
+ with NmsPlatformCredentialProviderHost
50
+ implements PeerOfficeConnection {
51
+ final adoptions = <p.RuntimeRequest>[];
52
+ final binds = <p.RuntimeRequest>[];
53
+ int _sequence = 0;
54
+
55
+ @override
56
+ Stream<p.RuntimeEvent> get runtimeEvents => const Stream.empty();
57
+ @override
58
+ int nextRuntimeRequestId() => ++_sequence;
59
+
60
+ @override
61
+ Future<p.RuntimeResponse> invokeRuntime(p.RuntimeRequest request) async {
62
+ if (request.hasCloseRealm()) {
63
+ return p.RuntimeResponse(
64
+ protocolVersion: 2,
65
+ requestId: request.requestId,
66
+ realmClosed: p.RealmClosed());
67
+ }
68
+ if (request.hasBindSession()) {
69
+ binds.add(request.deepCopy());
70
+ return p.RuntimeResponse(
71
+ protocolVersion: 2,
72
+ requestId: request.requestId,
73
+ sessionBound: p.SessionBound(
74
+ session: request.bindSession.session,
75
+ workspace: request.bindSession.workspace,
76
+ credentialCapability: 'capability:test'));
77
+ }
78
+ expect(request.hasAdoptApplicationModel(), isTrue);
79
+ adoptions.add(request.deepCopy());
80
+ return p.RuntimeResponse(
81
+ protocolVersion: 2,
82
+ requestId: request.requestId,
83
+ applicationModelAdopted: p.ApplicationModelAdopted(
84
+ workspace: request.adoptApplicationModel.session,
85
+ outcome: p.ModelAdoptionOutcome.MODEL_ADOPTION_OUTCOME_CURRENT));
86
+ }
87
+ }
88
+
89
+ NomosApprovedModel _model([String digit = '1']) => NomosApprovedModel(
90
+ lawSha256: digit * 64,
91
+ reason: 'test release',
92
+ domainKeys: const ['test'],
93
+ loadPackage: () async => Uint8List(0));
94
+
95
+ Future<void> _settle() => Future<void>.delayed(Duration.zero);
96
+
97
+ DartReception _reception(_Office office, NomosApprovedModel model) {
98
+ final reception = DartReception(office)
99
+ ..retainApprovedApplicationModel(model);
100
+ addTearDown(reception.dispose);
101
+ return reception;
102
+ }
103
+
104
+ void main() {
105
+ test(
106
+ 'repeated background requests share one invocation, not a cached verdict',
107
+ () async {
108
+ final office = _Office();
109
+ final reception = _reception(office, _model());
110
+ final session = reception.session('estate');
111
+ for (var count = 0; count < 20; count++) {
112
+ session.nomosInternalAdoptApplicationModelInBackground();
113
+ }
114
+ // Even another view of the same plane must not allocate another flight.
115
+ NomosSession(reception, 'estate')
116
+ .nomosInternalAdoptApplicationModelInBackground();
117
+ expect(office.calls, hasLength(1));
118
+ office.finish(0);
119
+ await _settle();
120
+ session.nomosInternalAdoptApplicationModelInBackground();
121
+ expect(office.calls, hasLength(2));
122
+ office.finish(1);
123
+ await _settle();
124
+ });
125
+
126
+ test('failed transport releases the flight and keeps the failure observable',
127
+ () async {
128
+ final office = _Office();
129
+ final reception = _reception(office, _model());
130
+ final session = reception.session('estate');
131
+ session.nomosInternalAdoptApplicationModelInBackground();
132
+ office.replies[0].completeError(StateError('offline'));
133
+ await _settle();
134
+ expect(session.currentStatus.modelAdoption?.detail, contains('offline'));
135
+ session.nomosInternalAdoptApplicationModelInBackground();
136
+ expect(office.calls, hasLength(2));
137
+ office.finish(1);
138
+ await _settle();
139
+ });
140
+
141
+ test('sessions, approved models and identity planes do not share flights',
142
+ () async {
143
+ final firstOffice = _Office();
144
+ final secondOffice = _Office();
145
+ final approved = _model();
146
+ final first = _reception(firstOffice, approved);
147
+ final second = _reception(secondOffice, approved);
148
+ first.session('estate').nomosInternalAdoptApplicationModelInBackground();
149
+ first.session('catalogue').nomosInternalAdoptApplicationModelInBackground();
150
+ second.session('estate').nomosInternalAdoptApplicationModelInBackground();
151
+ first.retainApprovedApplicationModel(_model('2'));
152
+ first.session('estate').nomosInternalAdoptApplicationModelInBackground();
153
+ expect(firstOffice.calls, hasLength(3));
154
+ expect(secondOffice.calls, hasLength(1));
155
+ expect(firstOffice.calls[2].adoptApplicationModel.lawSha256, '2' * 64);
156
+ for (var index = 0; index < 3; index++) {
157
+ firstOffice.finish(index);
158
+ }
159
+ secondOffice.finish(0);
160
+ await _settle();
161
+ });
162
+
163
+ test(
164
+ 'an explicit adoption remains an independent request with its own result',
165
+ () async {
166
+ final office = _Office();
167
+ final session = _reception(office, _model()).session('estate');
168
+ session.nomosInternalAdoptApplicationModelInBackground();
169
+ final explicit = session.adoptApplicationModel(dryRun: true);
170
+ expect(office.calls, hasLength(2));
171
+ expect(office.calls[1].adoptApplicationModel.dryRun, isTrue);
172
+ office.finish(1);
173
+ expect((await explicit).outcome, NomosModelAdoptionOutcome.current);
174
+ office.finish(0);
175
+ await _settle();
176
+ });
177
+
178
+ // THE OLD-LAW CHILD BOOTSTRAP TRIGGER (nomos2 architecture/old_law_child_bootstrap.md).
179
+ // A child whose law predates the parent-directed acceptor refuses every new device and has
180
+ // no operator door; the only key it accepts belongs to a device already seated in it. So
181
+ // BINDING the child IS the trigger, and it must not gate the caller on the outcome.
182
+ test('opening a child offers it to the model-adoption desk, once', () async {
183
+ final office = _BindingOffice();
184
+ final reception = DartReception(office)
185
+ ..retainApprovedApplicationModel(_model());
186
+ addTearDown(reception.dispose);
187
+
188
+ final child = await reception.bindWorkspace('estate-9c3d');
189
+ await _settle();
190
+ expect(office.binds.single.bindSession.workspace, 'estate-9c3d');
191
+ expect(office.adoptions.single.adoptApplicationModel.session, 'estate-9c3d');
192
+ expect(office.adoptions.single.adoptApplicationModel.dryRun, isFalse);
193
+
194
+ // Re-opening an already-bound child neither re-binds nor re-offers: the
195
+ // transition runs once, and afterwards the workspace carries the acceptor.
196
+ expect(identical(await reception.bindWorkspace('estate-9c3d'), child), isTrue);
197
+ await _settle();
198
+ expect(office.binds, hasLength(1));
199
+ expect(office.adoptions, hasLength(1));
200
+ });
201
+
202
+ test('a plane with no approved model opens children without offering any',
203
+ () async {
204
+ final office = _BindingOffice();
205
+ final reception = DartReception(office);
206
+ addTearDown(reception.dispose);
207
+ await reception.bindWorkspace('estate-9c3d');
208
+ await _settle();
209
+ expect(office.binds, hasLength(1));
210
+ expect(office.adoptions, isEmpty);
211
+ });
212
+ // Measured on a real device: only 2 of 8 workspace bindings went through `bindWorkspace`.
213
+ // A child reached by a foreign read or a credential-less follow binds through the raw
214
+ // primitive, and a trigger on `bindWorkspace` skipped exactly those — including the estate
215
+ // whose refusal started this work.
216
+ test('a child bound through the raw primitive is offered too, not only bindWorkspace',
217
+ () async {
218
+ final office = _BindingOffice();
219
+ final reception = DartReception(office)
220
+ ..retainApprovedApplicationModel(_model());
221
+ addTearDown(reception.dispose);
222
+
223
+ await reception.bindSession(
224
+ workspace: 'estate-9c3d', session: 'estate-9c3d');
225
+ await _settle();
226
+ expect(office.adoptions.single.adoptApplicationModel.session, 'estate-9c3d');
227
+ });
228
+ }
@@ -1,4 +1,4 @@
1
- // THE VERSION-FLOOR TRIPWIRE: the @githolon/client built by this checkout must satisfy the minimum
1
+ // THE VERSION-FLOOR TRIPWIRE: the @githolon/runtime built by this checkout must satisfy the minimum
2
2
  // runner version declared by nomos_client. Changesets may advance the npm package metadata without
3
3
  // changing its protocol, so a newer runner is valid; an older runner than the Dart floor is not.
4
4
  import 'dart:convert';
@@ -8,8 +8,8 @@ import 'package:nomos_client/src/nms_conformance_runtime.dart';
8
8
  import 'package:test/test.dart';
9
9
 
10
10
  void main() {
11
- test('kExpectedRunnerBundleVersion pairs with cloud/web-client/package.json', () {
12
- final f = File('${Directory.current.path}/../../cloud/web-client/package.json');
11
+ test('kExpectedRunnerBundleVersion pairs with peer-office/package/package.json', () {
12
+ final f = File('${Directory.current.path}/../../peer-office/package/package.json');
13
13
  if (!f.existsSync()) {
14
14
  markTestSkipped('monorepo layout not present (published-package test run)');
15
15
  return;
@@ -23,7 +23,7 @@ void main() {
23
23
  (runner[1] > floor[1] ||
24
24
  (runner[1] == floor[1] && runner[2] >= floor[2])));
25
25
  expect(runnerMeetsFloor, isTrue,
26
- reason: '@githolon/client $v is older than nomos_client runner floor '
26
+ reason: '@githolon/runtime $v is older than nomos_client runner floor '
27
27
  '$kExpectedRunnerBundleVersion');
28
28
  });
29
29
  }
@@ -0,0 +1,359 @@
1
+ import 'dart:async';
2
+ import 'dart:convert';
3
+ import 'dart:typed_data';
4
+
5
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
6
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
7
+ import 'package:test/test.dart';
8
+
9
+ final class _RepairTransport
10
+ with NmsPlatformCredentialProviderHost
11
+ implements
12
+ PeerOfficeConnection,
13
+ NomosKernelCallTransport,
14
+ NomosRuntimeV2EventTransport {
15
+ final _runtimeEvents = StreamController<protocol.RuntimeEvent>.broadcast();
16
+ protocol.CallRequest? lastCall;
17
+ protocol.CredentialUse? lastCredentialUse;
18
+ var providerToken = protocol.ProviderTokenVerificationStatus
19
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_NO_MATCHING_KEY;
20
+ var enrolledDevice = true;
21
+ var resolvedPrincipal = 'user:jack';
22
+ var syncCalls = 0;
23
+ final syncSessions = <String>[];
24
+ var offerCalls = 0;
25
+ var expireNextSessionOffer = false;
26
+ var refuseSignerEnrollmentOffers = 0;
27
+ var refuseKernelSignerOffers = 0;
28
+ String kernelRefusalActor = 'unenrolled-signer-key';
29
+ final refusedWorkspaces = <String>[];
30
+ final refreshedTokens = <String>[];
31
+ final authenticationRequests = <protocol.AwaitAuthenticationRequest>[];
32
+ protocol.AuthenticationReadiness? authenticationResult;
33
+
34
+ _RepairTransport() {
35
+ scheduleMicrotask(
36
+ () => _runtimeEvents.add(
37
+ protocol.RuntimeEvent(
38
+ ready: protocol.RuntimeReady(runtimeVersion: 'unbundled-dev'),
39
+ ),
40
+ ),
41
+ );
42
+ }
43
+
44
+ @override
45
+ Stream<protocol.RuntimeEvent> get runtimeEvents => _runtimeEvents.stream;
46
+
47
+ void pushRuntime(protocol.RuntimeEvent event) => _runtimeEvents.add(event);
48
+
49
+ int _requests = 0;
50
+ @override
51
+ int nextRuntimeRequestId() => ++_requests;
52
+
53
+ @override
54
+ Future<protocol.RuntimeResponse> invokeRuntime(
55
+ protocol.RuntimeRequest request,
56
+ ) async {
57
+ if (request.hasAwaitAuthentication()) {
58
+ authenticationRequests.add(request.awaitAuthentication.deepCopy());
59
+ return protocol.RuntimeResponse(
60
+ protocolVersion: 2,
61
+ requestId: request.requestId,
62
+ authenticationReadiness: authenticationResult ??
63
+ protocol.AuthenticationReadiness(
64
+ outcome: protocol.AuthenticationReadinessOutcome
65
+ .AUTHENTICATION_READINESS_OUTCOME_READY,
66
+ posture: protocol.AuthenticationPostureResponse(
67
+ providerToken: protocol.ProviderTokenVerificationStatus
68
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_VALID,
69
+ enrolledDevice: true,
70
+ resolvedPrincipal: 'user:jack',
71
+ ),
72
+ ),
73
+ );
74
+ }
75
+ if (request.hasBindSession()) {
76
+ return protocol.RuntimeResponse(
77
+ protocolVersion: 2,
78
+ requestId: request.requestId,
79
+ sessionBound: protocol.SessionBound(
80
+ workspace: request.bindSession.workspace,
81
+ session: request.bindSession.hasSession()
82
+ ? request.bindSession.session
83
+ : request.bindSession.workspace,
84
+ credentialCapability: request.bindSession.hasSession()
85
+ ? request.bindSession.session
86
+ : request.bindSession.workspace,
87
+ ),
88
+ );
89
+ }
90
+ if (request.hasSyncWorkspace()) {
91
+ syncCalls += 1;
92
+ syncSessions.add(request.syncWorkspace.workspace);
93
+ providerToken = protocol.ProviderTokenVerificationStatus
94
+ .PROVIDER_TOKEN_VERIFICATION_STATUS_VALID;
95
+ return protocol.RuntimeResponse(
96
+ protocolVersion: 2,
97
+ requestId: request.requestId,
98
+ syncWorkspace: protocol.SyncWorkspaceResponse(
99
+ workspace: request.syncWorkspace.workspace,
100
+ ),
101
+ );
102
+ }
103
+ if (request.hasRefreshCredentials()) {
104
+ refreshedTokens.add(utf8.decode(request.refreshCredentials.authToken));
105
+ return protocol.RuntimeResponse(
106
+ protocolVersion: 2,
107
+ requestId: request.requestId,
108
+ credentialsRefreshed: protocol.CredentialsRefreshed(
109
+ session: request.refreshCredentials.session,
110
+ hasAuthToken: request.refreshCredentials.authToken.isNotEmpty,
111
+ ),
112
+ );
113
+ }
114
+ return protocol.RuntimeResponse(
115
+ protocolVersion: 2,
116
+ requestId: request.requestId,
117
+ realmClosed: protocol.RealmClosed(),
118
+ );
119
+ }
120
+
121
+ @override
122
+ Future<protocol.CallResponse> callKernel({
123
+ required String session,
124
+ required protocol.CallRequest request,
125
+ protocol.CredentialUse credentialUse =
126
+ protocol.CredentialUse.CREDENTIAL_USE_UNSPECIFIED,
127
+ }) async {
128
+ lastCall = request.deepCopy();
129
+ lastCredentialUse = credentialUse;
130
+ if (request.hasQuery() && request.query.hasAuthenticationPosture()) {
131
+ return protocol.CallResponse(
132
+ protocolVersion: 1,
133
+ requestId: request.requestId,
134
+ query: protocol.QueryResponse(
135
+ authenticationPosture: protocol.AuthenticationPostureResponse(
136
+ providerToken: providerToken,
137
+ enrolledDevice: enrolledDevice,
138
+ resolvedPrincipal: resolvedPrincipal,
139
+ ),
140
+ ),
141
+ );
142
+ }
143
+ if (request.hasOffer()) {
144
+ offerCalls += 1;
145
+ if (refuseKernelSignerOffers > 0) {
146
+ refuseKernelSignerOffers -= 1;
147
+ return protocol.CallResponse(
148
+ protocolVersion: 1,
149
+ requestId: request.requestId,
150
+ offer: protocol.OfferResponse(
151
+ refused: protocol.OfferRefused(
152
+ refusal: protocol.KernelRefusal(
153
+ code: protocol
154
+ .KernelRefusalCode.KERNEL_REFUSAL_CODE_FORGED_PRINCIPAL,
155
+ terminal: true,
156
+ message: 'verified author is $kernelRefusalActor',
157
+ details: protocol.NomosStruct(fields: [
158
+ MapEntry(
159
+ 'actor', protocol.NomosValue(text: kernelRefusalActor)),
160
+ MapEntry('claimed', protocol.NomosValue(text: 'user:jack')),
161
+ ]),
162
+ ),
163
+ ),
164
+ ),
165
+ );
166
+ }
167
+ if (refuseSignerEnrollmentOffers > 0) {
168
+ refuseSignerEnrollmentOffers -= 1;
169
+ refusedWorkspaces.add(session);
170
+ return protocol.CallResponse(
171
+ protocolVersion: 1,
172
+ requestId: request.requestId,
173
+ failure: protocol.Failure(
174
+ code: 'nomos.authz.signer_enrollment_missing',
175
+ message:
176
+ 'auth: ordinary user offers require both a valid provider token and an active enrolled device',
177
+ ),
178
+ );
179
+ }
180
+ if (expireNextSessionOffer &&
181
+ credentialUse == protocol.CredentialUse.CREDENTIAL_USE_SESSION) {
182
+ expireNextSessionOffer = false;
183
+ return protocol.CallResponse(
184
+ protocolVersion: 1,
185
+ requestId: request.requestId,
186
+ failure: protocol.Failure(
187
+ code: 'nomos.auth.token_expired',
188
+ message: 'auth: token verification failed (Expired)',
189
+ ),
190
+ );
191
+ }
192
+ }
193
+ return protocol.CallResponse(
194
+ protocolVersion: 1,
195
+ requestId: request.requestId,
196
+ offer: protocol.OfferResponse(
197
+ admitted: protocol.OfferAdmitted(
198
+ head: 'HEAD-offer',
199
+ intentId: 'intent-offer',
200
+ ),
201
+ ),
202
+ );
203
+ }
204
+ }
205
+
206
+ NmsGeneratedCall _estateCall() => NmsInternalGeneratedCalls.create(
207
+ domain: 'estate',
208
+ directive: 'ensureSiteComplianceFolders',
209
+ protobufType:
210
+ 'type.nomos.dev/nomos.operation.v1.Payload_ensure_site_compliance_test',
211
+ protobufPayload: Uint8List.fromList([1]),
212
+ );
213
+
214
+ Future<DartReception> _connected(_RepairTransport transport) async {
215
+ final bridge = DartReception(transport);
216
+ await bridge.ready;
217
+ await bridge.bindSession(workspace: 'home', session: 'home');
218
+ await bridge.session('home').refreshCredentials(authToken: 'token');
219
+ await bridge.bindSession(
220
+ workspace: 'estate-1',
221
+ session: 'estate-1',
222
+ );
223
+ await bridge.session('estate-1').refreshCredentials(authToken: 'token');
224
+ return bridge;
225
+ }
226
+
227
+ void main() {
228
+ test('kernel unenrolled signer refusal repairs the child and retries once',
229
+ () async {
230
+ final transport = _RepairTransport()..refuseKernelSignerOffers = 1;
231
+ final bridge = await _connected(transport);
232
+ addTearDown(bridge.dispose);
233
+ final repairs = <ParentSignerEnrollmentMissing>[];
234
+ bridge.adoptChildAuthenticationRepair((refusal) async {
235
+ repairs.add(refusal);
236
+ return true;
237
+ });
238
+ await bridge
239
+ .session('estate-1')
240
+ .offerGeneratedCallOutcome(call: _estateCall());
241
+ expect(transport.offerCalls, 2);
242
+ expect(repairs.single.workspace, 'estate-1');
243
+ expect(repairs.single.actor, 'user:jack');
244
+ expect(repairs.single.directiveId, 'ensureSiteComplianceFolders');
245
+ });
246
+
247
+ test('other forged-principal refusals never trigger child repair', () async {
248
+ final transport = _RepairTransport()
249
+ ..refuseKernelSignerOffers = 1
250
+ ..kernelRefusalActor = 'different-author';
251
+ final bridge = await _connected(transport);
252
+ addTearDown(bridge.dispose);
253
+ var repairs = 0;
254
+ bridge.adoptChildAuthenticationRepair((_) async {
255
+ repairs += 1;
256
+ return true;
257
+ });
258
+ await expectLater(
259
+ bridge.session('estate-1').offerGeneratedCallOutcome(call: _estateCall()),
260
+ throwsA(isA<NomosTypedOfferFailure>()),
261
+ );
262
+ expect(repairs, 0);
263
+ expect(transport.offerCalls, 1);
264
+ });
265
+
266
+ test('failed kernel signer repair preserves the original refusal', () async {
267
+ final transport = _RepairTransport()..refuseKernelSignerOffers = 1;
268
+ final bridge = await _connected(transport);
269
+ addTearDown(bridge.dispose);
270
+ bridge.adoptChildAuthenticationRepair((_) async => false);
271
+ await expectLater(
272
+ bridge.session('estate-1').offerGeneratedCallOutcome(call: _estateCall()),
273
+ throwsA(isA<NomosTypedOfferFailure>()),
274
+ );
275
+ expect(transport.offerCalls, 1);
276
+ });
277
+
278
+ test('a registered repair that seats the key makes the offer succeed once',
279
+ () async {
280
+ final transport = _RepairTransport()..refuseSignerEnrollmentOffers = 1;
281
+ final bridge = await _connected(transport);
282
+ addTearDown(bridge.dispose);
283
+ final repairs = <ParentSignerEnrollmentMissing>[];
284
+ bridge.session('home').adoptChildAuthenticationRepair((refusal) async {
285
+ repairs.add(refusal);
286
+ return true;
287
+ });
288
+ final outcome = await bridge
289
+ .session('estate-1')
290
+ .offerGeneratedCallOutcome(call: _estateCall());
291
+ expect(outcome, isNotNull);
292
+ expect(transport.offerCalls, 2);
293
+ expect(repairs, hasLength(1));
294
+ expect(repairs.single.workspace, 'estate-1');
295
+ expect(repairs.single.code, 'nomos.authz.signer_enrollment_missing');
296
+ });
297
+
298
+ test('a repair that cannot seat the key surfaces the original refusal',
299
+ () async {
300
+ final transport = _RepairTransport()..refuseSignerEnrollmentOffers = 1;
301
+ final bridge = await _connected(transport);
302
+ addTearDown(bridge.dispose);
303
+ bridge.adoptChildAuthenticationRepair((_) async => false);
304
+ await expectLater(
305
+ bridge.session('estate-1').offerGeneratedCallOutcome(call: _estateCall()),
306
+ throwsA(isA<ParentSignerEnrollmentMissing>()
307
+ .having((error) => error.workspace, 'workspace', 'estate-1')),
308
+ );
309
+ expect(transport.offerCalls, 1);
310
+ });
311
+
312
+ test('a repair that throws never masks the refusal or retries', () async {
313
+ final transport = _RepairTransport()..refuseSignerEnrollmentOffers = 1;
314
+ final bridge = await _connected(transport);
315
+ addTearDown(bridge.dispose);
316
+ bridge.adoptChildAuthenticationRepair(
317
+ (_) async => throw StateError('home is offline'),
318
+ );
319
+ await expectLater(
320
+ bridge.session('estate-1').offerGeneratedCallOutcome(call: _estateCall()),
321
+ throwsA(isA<ParentSignerEnrollmentMissing>()),
322
+ );
323
+ expect(transport.offerCalls, 1);
324
+ });
325
+
326
+ test('without a registration the refusal is untouched', () async {
327
+ final transport = _RepairTransport()..refuseSignerEnrollmentOffers = 1;
328
+ final bridge = await _connected(transport);
329
+ addTearDown(bridge.dispose);
330
+ await expectLater(
331
+ bridge.session('estate-1').offerGeneratedCallOutcome(call: _estateCall()),
332
+ throwsA(isA<ParentSignerEnrollmentMissing>()),
333
+ );
334
+ expect(transport.offerCalls, 1);
335
+ });
336
+
337
+ test('concurrent refusals for one workspace share a single repair', () async {
338
+ final transport = _RepairTransport()..refuseSignerEnrollmentOffers = 2;
339
+ final bridge = await _connected(transport);
340
+ addTearDown(bridge.dispose);
341
+ var repairs = 0;
342
+ final gate = Completer<bool>();
343
+ bridge.adoptChildAuthenticationRepair((_) {
344
+ repairs += 1;
345
+ return gate.future;
346
+ });
347
+ final first = bridge
348
+ .session('estate-1')
349
+ .offerGeneratedCallOutcome(call: _estateCall());
350
+ final second = bridge
351
+ .session('estate-1')
352
+ .offerGeneratedCallOutcome(call: _estateCall());
353
+ await Future<void>.delayed(Duration.zero);
354
+ gate.complete(true);
355
+ await Future.wait([first, second]);
356
+ expect(repairs, 1);
357
+ expect(transport.offerCalls, 4);
358
+ });
359
+ }