@githolon/dsl 0.108.7 → 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 +1994 -1189
  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,419 @@
1
+ import 'package:nomos_client/src/sync_status_projection.dart';
2
+ import 'package:nomos_client/src/sync_status_reducer.dart';
3
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
4
+ import 'package:fixnum/fixnum.dart' as fixnum;
5
+ import 'package:test/test.dart';
6
+
7
+ void main() {
8
+ final at = DateTime.utc(2026, 9, 7);
9
+ SyncStatus reduce(protocol.SyncWorkspaceResponse result,
10
+ [SyncStatus current = const SyncStatus()]) =>
11
+ reduceSyncCompletion(current, protocol.SyncCompleted(result: result), at);
12
+
13
+ test('cold-start status is unknown, not current with zero debt', () {
14
+ const status = SyncStatus();
15
+ expect(status.phase, SyncPhase.checking);
16
+ expect(status.ahead, isNull);
17
+ expect(status.lastSyncedAt, isNull);
18
+ });
19
+
20
+ test('read-only transfer completion never invents unsaved work or currency',
21
+ () {
22
+ for (final kind in ['full', 'delta', 'push']) {
23
+ final settled =
24
+ reduce(protocol.SyncWorkspaceResponse(currentHead: 'head'));
25
+ final started = reduceTransferProgress(
26
+ settled, TransferProgress(received: 4, total: 10, kind: kind));
27
+ expect(started.phase, SyncPhase.checking);
28
+ expect(started.presentationPhase, SyncPhase.transferring);
29
+ final done = reduceTransferProgress(
30
+ started, TransferProgress(received: 10, total: 10, kind: kind));
31
+ expect(done.phase, SyncPhase.checking);
32
+ expect(done.presentationPhase, SyncPhase.checking);
33
+ expect(done.transfer, isNull);
34
+ expect(done.ahead, isNull);
35
+ expect(done.lastSyncedAt, at);
36
+ }
37
+ });
38
+
39
+ test(
40
+ 'transfer overlay preserves actual local work, failure and offline state',
41
+ () {
42
+ for (final phase in [
43
+ SyncPhase.syncing,
44
+ SyncPhase.error,
45
+ SyncPhase.offline
46
+ ]) {
47
+ final initial =
48
+ SyncStatus(phase: phase, lastError: 'retained explanation');
49
+ final started = reduceTransferProgress(
50
+ initial, const TransferProgress(received: 4, total: 10));
51
+ final done = reduceTransferProgress(
52
+ started, const TransferProgress(received: 10, total: 10));
53
+ expect(done.phase, phase);
54
+ expect(done.lastError, 'retained explanation');
55
+ }
56
+ final admittedDuringTransfer = locallyAdmitted(reduceTransferProgress(
57
+ const SyncStatus(), const TransferProgress(received: 1, total: 10)));
58
+ expect(
59
+ reduceTransferProgress(admittedDuringTransfer,
60
+ const TransferProgress(received: 10, total: 10))
61
+ .phase,
62
+ SyncPhase.syncing);
63
+ });
64
+
65
+ test('aggregate progress clears to unknown rather than manufactured saving',
66
+ () async {
67
+ final projection = NmsSyncStatusProjection();
68
+ addTearDown(projection.close);
69
+ projection.pushSession(
70
+ 'catalogue',
71
+ reduceTransferProgress(
72
+ const SyncStatus(), const TransferProgress(received: 1, total: 2)));
73
+ projection.foldApplication(['catalogue']);
74
+ expect(projection.applicationStatus.phase, SyncPhase.transferring);
75
+ final completed = reduceTransferProgress(projection.applicationStatus,
76
+ const TransferProgress(received: 2, total: 2));
77
+ expect(completed.phase, SyncPhase.checking);
78
+ expect(completed.transfer, isNull);
79
+ });
80
+
81
+ test('unsettled runtime result reconstructs pending without callback counts',
82
+ () {
83
+ final status = reduce(protocol.SyncWorkspaceResponse(stillAhead: true));
84
+ expect(status.phase, SyncPhase.syncing);
85
+ expect(status.ahead, isNull);
86
+ expect(status.lastSyncedAt, isNull);
87
+ });
88
+
89
+ test('only a settled result establishes zero and the successful-sync time',
90
+ () {
91
+ final status = reduce(protocol.SyncWorkspaceResponse(currentHead: 'head'));
92
+ expect(status.phase, SyncPhase.idle);
93
+ expect(status.ahead, 0);
94
+ expect(status.behind, 0);
95
+ expect(status.saved, isNull);
96
+ expect(status.onMain, isNull);
97
+ expect(status.lastSyncedAt, at);
98
+ });
99
+
100
+ test('unsealed remote admission is not unsent local work or confirmed sync',
101
+ () async {
102
+ final pending = protocol.SyncWorkspaceResponse(
103
+ currentHead: 'shared-head',
104
+ stillAhead: true,
105
+ offerReceipt: protocol.CustodyOfferReceipt(
106
+ admission: protocol.CustodyOfferAdmissionReceipt(
107
+ main: 'shared-head', sealedMain: 'older-head')),
108
+ );
109
+ final status = reduce(pending);
110
+ expect(status.phase, SyncPhase.awaitingRemoteDurability);
111
+ expect(status.ahead, isNull);
112
+ expect(status.lastSyncedAt, isNull);
113
+ final projection = NmsSyncStatusProjection();
114
+ addTearDown(projection.close);
115
+ projection.pushSession('catalogue', status);
116
+ projection.foldApplication(['catalogue']);
117
+ expect(
118
+ projection.applicationStatus.phase, SyncPhase.awaitingRemoteDurability);
119
+ pending.currentHead = 'newer-local-head';
120
+ expect(reduce(pending).phase, SyncPhase.syncing);
121
+ pending.currentHead = 'shared-head';
122
+ pending.stillAhead = false;
123
+ pending.offerReceipt.admission.sealedMain = 'shared-head';
124
+ expect(reduce(pending).phase, SyncPhase.idle);
125
+ });
126
+
127
+ test(
128
+ 'local admission invalidates settled counts without maintaining a mirror',
129
+ () {
130
+ final current = reduce(protocol.SyncWorkspaceResponse(currentHead: 'head'));
131
+ final status = locallyAdmitted(locallyAdmitted(current));
132
+ expect(status.phase, SyncPhase.syncing);
133
+ expect(status.ahead, isNull);
134
+ });
135
+
136
+ test(
137
+ 'offline preserves the last successful time and exposes discovery failure',
138
+ () {
139
+ final earlier = DateTime.utc(2026, 9, 6);
140
+ final status = reduce(
141
+ protocol.SyncWorkspaceResponse(
142
+ offline: true,
143
+ stillAhead: true,
144
+ discoveryError:
145
+ protocol.RuntimeTransportFailure(message: 'DNS unavailable'),
146
+ ),
147
+ SyncStatus(lastSyncedAt: earlier));
148
+ expect(status.phase, SyncPhase.offline);
149
+ expect(status.lastSyncedAt, earlier);
150
+ expect(status.lastError, 'DNS unavailable');
151
+ });
152
+
153
+ test('push refusal remains attention even with no local callback counter',
154
+ () {
155
+ final status = reduce(protocol.SyncWorkspaceResponse(
156
+ pushFailed: true,
157
+ stillAhead: true,
158
+ pushError: protocol.RuntimeTransportFailure(message: 'Offer refused'),
159
+ ));
160
+ expect(status.phase, SyncPhase.error);
161
+ expect(status.lastError, 'Offer refused');
162
+ });
163
+
164
+ test(
165
+ 'terminal reconciliation failure preserves prior receipt and observation age',
166
+ () {
167
+ final earlier = DateTime.utc(2026, 9, 16, 16);
168
+ final receipt =
169
+ protocol.SyncWorkspaceResponse(currentHead: 'confirmed-head');
170
+ final current = SyncStatus(
171
+ phase: SyncPhase.syncing, lastSyncedAt: earlier, lastResult: receipt);
172
+ final failed = reduceSyncCompletion(
173
+ current,
174
+ protocol.SyncCompleted(
175
+ failure: protocol.RuntimeFailure(
176
+ code: 'nomos.runtime.sync_failed',
177
+ message: 'workspace-sync-1: reconciliation failed'),
178
+ ),
179
+ DateTime.utc(2026, 9, 16, 17));
180
+ expect(failed.phase, SyncPhase.error);
181
+ expect(failed.lastSyncedAt, earlier);
182
+ expect(identical(failed.lastResult, receipt), isTrue);
183
+ expect(failed.lastError, contains('workspace-sync-1'));
184
+ expect(failed.transfer, isNull);
185
+ });
186
+
187
+ test('local-only and reconnect outcomes are not cloud convergence', () {
188
+ for (final result in [
189
+ protocol.SyncWorkspaceResponse(localOnly: true),
190
+ protocol.SyncWorkspaceResponse(reconnectRequired: true),
191
+ ]) {
192
+ expect(reduce(result).phase, SyncPhase.checking);
193
+ expect(reduce(result).lastSyncedAt, isNull);
194
+ }
195
+ expect(
196
+ reduce(protocol.SyncWorkspaceResponse(directOfferDeferred: true)).phase,
197
+ SyncPhase.syncing);
198
+ });
199
+
200
+ test('deferred admission preserves the remote explanation', () {
201
+ final status = reduce(protocol.SyncWorkspaceResponse(
202
+ stillAhead: true,
203
+ offerReceipt: protocol.CustodyOfferReceipt(
204
+ admission: protocol.CustodyOfferAdmissionReceipt(sessions: [
205
+ protocol.CustodySessionAdmission(intents: [
206
+ protocol.CustodyIntentAdmission(
207
+ deadLettered: protocol.CustodyIntentDeadLettered(
208
+ message: 'Required source workspace is unavailable',
209
+ ),
210
+ ),
211
+ ]),
212
+ ]),
213
+ ),
214
+ ));
215
+ expect(status.phase, SyncPhase.error);
216
+ expect(status.lastError, 'Required source workspace is unavailable');
217
+ expect(status.ahead, isNull);
218
+ });
219
+
220
+ test('blocked custody offer names the remote dependency', () {
221
+ final status = reduce(protocol.SyncWorkspaceResponse(
222
+ stillAhead: true,
223
+ directOfferDeferred: true,
224
+ offerReceipt: protocol.CustodyOfferReceipt(
225
+ blocked: protocol.RuntimeBlocked(
226
+ siblings: protocol.SiblingResidenciesRequired(residencies: [
227
+ protocol.SiblingResidencyRequired(workspace: 'catalogue-source'),
228
+ ]),
229
+ ),
230
+ ),
231
+ ));
232
+ expect(status.phase, SyncPhase.error);
233
+ expect(status.lastError, contains('catalogue-source'));
234
+ });
235
+
236
+ test('typed offer failure cannot be presented as ordinary pending', () {
237
+ final status = reduce(protocol.SyncWorkspaceResponse(
238
+ stillAhead: true,
239
+ offerReceipt: protocol.CustodyOfferReceipt(
240
+ failure: protocol.CustodyOfferFailure(message: 'Admission unavailable'),
241
+ ),
242
+ ));
243
+ expect(status.phase, SyncPhase.error);
244
+ expect(status.lastError, 'Admission unavailable');
245
+ });
246
+
247
+ test('remote refusal retains code, reason, intent and immutable receipt', () {
248
+ final result = protocol.SyncWorkspaceResponse(
249
+ currentHead: 'local-head',
250
+ remoteRefusal: protocol.SyncRefusal(
251
+ code: 'REFERENCE_TYPE_MISMATCH',
252
+ reason: 'catalogueWorkspace must be a reference',
253
+ intentId: 'offer-17',
254
+ ),
255
+ );
256
+ final status = reduce(result);
257
+ expect(status.phase, SyncPhase.error);
258
+ expect(status.lastError, contains('REFERENCE_TYPE_MISMATCH'));
259
+ expect(
260
+ status.lastError, contains('catalogueWorkspace must be a reference'));
261
+ expect(status.lastError, contains('offer-17'));
262
+ result.currentHead = 'mutated';
263
+ expect(status.lastResult!.currentHead, 'local-head');
264
+ expect(status.lastResult!.isFrozen, isTrue);
265
+ });
266
+
267
+ test('custody disagreement retains both heads and does not invent counts',
268
+ () {
269
+ final status = reduce(protocol.SyncWorkspaceResponse(
270
+ custodyDisagreement: protocol.CustodyDisagreement(
271
+ localHead: 'local',
272
+ candidateHead: 'remote',
273
+ reason: 'Not a fast forward',
274
+ ),
275
+ ));
276
+ expect(status.lastError, contains('Not a fast forward'));
277
+ expect(status.lastError, contains('Local: local'));
278
+ expect(status.lastError, contains('Remote: remote'));
279
+ expect(status.ahead, isNull);
280
+ expect(status.lastResult!.custodyReplaced, isFalse);
281
+ });
282
+
283
+ test('per-session changes notify even when application summary is unchanged',
284
+ () async {
285
+ final projection = NmsSyncStatusProjection();
286
+ addTearDown(projection.close);
287
+ projection.ensureSession('home', const SyncStatus(phase: SyncPhase.error));
288
+ projection.ensureSession('estate', const SyncStatus());
289
+ projection.foldApplication(['home', 'estate']);
290
+ final application = projection.applicationStatus;
291
+ final changed = projection.changes.first;
292
+ projection.pushSession(
293
+ 'estate', const SyncStatus(lastError: 'Exact issue'));
294
+ await changed;
295
+ expect(projection.statusFor('estate').lastError, 'Exact issue');
296
+ expect(identical(application, projection.applicationStatus), isTrue);
297
+ });
298
+
299
+ test('missing result is a protocol failure, not a default success', () {
300
+ expect(
301
+ reduceSyncCompletion(const SyncStatus(), protocol.SyncCompleted(), at)
302
+ .phase,
303
+ SyncPhase.error);
304
+ });
305
+
306
+ test('an empty result does not establish a confirmed frontier', () {
307
+ final status = reduce(protocol.SyncWorkspaceResponse());
308
+ expect(status.phase, SyncPhase.checking);
309
+ expect(status.ahead, isNull);
310
+ expect(status.lastSyncedAt, isNull);
311
+ });
312
+
313
+ test('aggregation retains unknown sessions and never fabricates totals',
314
+ () async {
315
+ final projection = NmsSyncStatusProjection();
316
+ addTearDown(projection.close);
317
+ projection.pushSession(
318
+ 'estate', reduce(protocol.SyncWorkspaceResponse(currentHead: 'head')));
319
+ projection.foldApplication(['estate', 'home']);
320
+ expect(projection.applicationStatus.phase, SyncPhase.checking);
321
+ expect(projection.applicationStatus.ahead, isNull);
322
+ expect(projection.applicationStatus.lastSyncedAt, isNull);
323
+ projection.pushSession(
324
+ 'home', reduce(protocol.SyncWorkspaceResponse(stillAhead: true)));
325
+ projection.foldApplication(['estate', 'home']);
326
+ expect(projection.applicationStatus.phase, SyncPhase.syncing);
327
+ projection.foldApplication([]);
328
+ expect(projection.applicationStatus.phase, SyncPhase.checking);
329
+ });
330
+
331
+ test(
332
+ 'a settled workspace with an open live link is current now, not as of its last round',
333
+ () async {
334
+ final projection = NmsSyncStatusProjection();
335
+ addTearDown(projection.close);
336
+ final settled = reduce(protocol.SyncWorkspaceResponse(
337
+ previousHead: 'head', currentHead: 'head'));
338
+ expect(settled.phase, SyncPhase.idle, reason: 'fixture must be settled');
339
+ expect(settled.lastSyncedAt, at);
340
+ projection.pushSession('estate', settled);
341
+ projection.pushSession('home', settled.copyWith(live: true));
342
+ projection.foldApplication(['estate', 'home']);
343
+ expect(projection.applicationStatus.live, isFalse);
344
+ expect(projection.applicationStatus.lastSyncedAt, at,
345
+ reason: 'the quiet, link-less workspace is the oldest confirmation');
346
+ projection.pushSession('estate', settled.copyWith(live: true));
347
+ projection.foldApplication(['estate', 'home']);
348
+ expect(projection.applicationStatus.live, isTrue);
349
+ expect(projection.applicationStatus.lastSyncedAt!.isAfter(at), isTrue,
350
+ reason: 'every workspace is live: confirmed as of now');
351
+ expect(
352
+ reduce(
353
+ protocol.SyncWorkspaceResponse(
354
+ previousHead: 'head', currentHead: 'next'),
355
+ settled.copyWith(live: true))
356
+ .live,
357
+ isTrue,
358
+ reason: 'a completed round never forgets the link state');
359
+ });
360
+
361
+ test(
362
+ 'the runtime follow inventory survives every re-fold of the application status',
363
+ () async {
364
+ final projection = NmsSyncStatusProjection();
365
+ addTearDown(projection.close);
366
+ const follow = WorkspaceFollowStatus(
367
+ workspace: 'estate-example',
368
+ phase: WorkspaceFollowPhase.running,
369
+ live: false,
370
+ );
371
+ projection.updateFollows(9, 7, live: 6, statuses: [follow]);
372
+ expect(projection.applicationStatus.followed, 9);
373
+ expect(projection.applicationStatus.followsInSync, 7);
374
+ expect(projection.applicationStatus.followsLive, 6);
375
+ expect(projection.applicationStatus.followStatuses.single.workspace,
376
+ 'estate-example');
377
+ projection.pushSession(
378
+ 'estate',
379
+ reduce(protocol.SyncWorkspaceResponse(
380
+ previousHead: 'head', currentHead: 'head')));
381
+ projection.foldApplication(['estate']);
382
+ expect(projection.applicationStatus.followed, 9);
383
+ expect(projection.applicationStatus.followsInSync, 7);
384
+ expect(projection.applicationStatus.followStatuses.single.phase,
385
+ WorkspaceFollowPhase.running);
386
+ projection.foldApplication([]);
387
+ expect(projection.applicationStatus.followsInSync, 7);
388
+ expect(projection.applicationStatus.followsLive, 6);
389
+ expect(projection.applicationStatus.followStatuses.single.live, isFalse);
390
+ });
391
+
392
+ test('follow report keeps the peer operation, schedule and source time',
393
+ () async {
394
+ final projection = NmsSyncStatusProjection();
395
+ addTearDown(projection.close);
396
+ final observedAt = DateTime.fromMillisecondsSinceEpoch(1800000000000);
397
+ final due = WorkspaceFollowStatus.fromRuntime(protocol.FollowObservation(
398
+ workspace: 'catalogue',
399
+ phase: protocol.FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_DUE,
400
+ dueAtUnixMillis: fixnum.Int64(1800000060000),
401
+ ));
402
+ final running =
403
+ WorkspaceFollowStatus.fromRuntime(protocol.FollowObservation(
404
+ workspace: 'estate',
405
+ phase: protocol.FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_RUNNING,
406
+ attempt: 2,
407
+ activeEffect: 'gitFetch',
408
+ ));
409
+ projection.updateFollows(2, 0,
410
+ statuses: [due, running], observedAt: observedAt);
411
+ projection.foldApplication(const []);
412
+ final status = projection.applicationStatus;
413
+ expect(status.followReportAt, observedAt);
414
+ expect(status.followStatuses.first.dueAt,
415
+ DateTime.fromMillisecondsSinceEpoch(1800000060000));
416
+ expect(status.followStatuses.last.attempt, 2);
417
+ expect(status.followStatuses.last.activeEffect, 'gitFetch');
418
+ });
419
+ }
@@ -1,10 +1,10 @@
1
1
  // NomosTestHarness — the EPHEMERAL, FULLY-OFFLINE fixture loop: install real tenant law into a fresh local
2
2
  // workspace (no cloud), drive one directive + one read through the REAL githolon over the node runner, and
3
- // tear down deterministically. This is the headless app-test lane: the same bridge + generated-client surface
3
+ // tear down deterministically. This is the UI-less app-test lane: the same bridge + generated-client surface
4
4
  // the app UI uses, under `dart test` / `flutter test`, with no network.
5
5
  //
6
6
  // Prerequisites (skipped-with-reason when absent, so CI stays green either way): `node` on PATH; the
7
- // in-repo runner; a warm `~/.holon/runtime` cache (holon-testmint.wasm + bootstrap.law.usdz + nomos.law.usdz — one `githolon` run fills it);
7
+ // in-repo runner; a warm `~/.holon/runtime` cache (holon.wasm + bootstrap.law.usdz + nomos.law.usdz — one `githolon` run fills it);
8
8
  // and the compiled `examples/guestbook/build/guestbook.law.usdz` (`cd examples/guestbook && npx nomos-compile`).
9
9
  @TestOn('vm')
10
10
  library;
@@ -18,15 +18,15 @@ import 'package:nomos_types/nomos_kernel_protocol.dart'
18
18
  import 'package:test/test.dart';
19
19
 
20
20
  String get _repoEntry =>
21
- '${Directory.current.path}/../../cloud/web-client/runner/node-entry.ts';
21
+ '${Directory.current.path}/../../peer-office/package/runner/node-entry.mjs';
22
22
  String get _guestbookLawUsdz =>
23
23
  '${Directory.current.path}/../../examples/guestbook/build/guestbook.law.usdz';
24
24
  String get _runtimeDir =>
25
+ // Same precedence as the harness itself: an explicit candidate runtime first.
26
+ Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
25
27
  Platform.environment['NOMOS_DART_RUNTIME'] ??
26
28
  Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
27
29
  '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
28
- // The built CLI bin — drives the `githolon harness-install` SUBCOMMAND lane (the published-harness path).
29
- String get _cliDist => '${Directory.current.path}/../../cli/dist/cli.mjs';
30
30
 
31
31
  /// The prerequisite that isn't installable in-test (a warm runtime cache + the compiled fixture). Returns a
32
32
  /// skip reason, or null when everything the harness needs is present.
@@ -36,7 +36,7 @@ String? _missingPrereq() {
36
36
  if (!File(_guestbookLawUsdz).existsSync()) {
37
37
  return 'compile the fixture first: cd examples/guestbook && npx nomos-compile';
38
38
  }
39
- if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
39
+ if (!File('$_runtimeDir/holon.wasm').existsSync() ||
40
40
  !File('$_runtimeDir/bootstrap.law.usdz').existsSync() ||
41
41
  !File('$_runtimeDir/nomos.law.usdz').existsSync()) {
42
42
  return 'provide the coordinated candidate with NOMOS_DART_RUNTIME, or warm ~/.holon/runtime once';
@@ -45,6 +45,23 @@ String? _missingPrereq() {
45
45
  }
46
46
 
47
47
  void main() {
48
+ test('remote convergence requires an actual settled frontier', () {
49
+ expect(nmsRemoteConvergenceSettled(SyncWorkspaceResponse()), isFalse);
50
+ expect(nmsRemoteConvergenceSettled(SyncWorkspaceResponse(
51
+ currentHead: 'settled-head',
52
+ )), isTrue);
53
+ for (final pending in <SyncWorkspaceResponse>[
54
+ SyncWorkspaceResponse(currentHead: 'head', reconnectRequired: true),
55
+ SyncWorkspaceResponse(currentHead: 'head', directOfferDeferred: true),
56
+ SyncWorkspaceResponse(currentHead: 'head', localOnly: true),
57
+ SyncWorkspaceResponse(currentHead: 'head', discoveryError: RuntimeTransportFailure(status: 503)),
58
+ SyncWorkspaceResponse(currentHead: 'head', deferred: Int64.ONE),
59
+ SyncWorkspaceResponse(currentHead: 'head', refused: Int64.ONE),
60
+ ]) {
61
+ expect(nmsRemoteConvergenceSettled(pending), isFalse, reason: pending.toString());
62
+ }
63
+ });
64
+
48
65
  test(
49
66
  'convergence never reports rejected or dead-lettered business work as success',
50
67
  () {
@@ -71,7 +88,7 @@ void main() {
71
88
  );
72
89
  });
73
90
 
74
- test('OFFLINE: install real law, write one directive, read it back, dispose',
91
+ test('OFFLINE: install real law into Git custody and cold-remount it',
75
92
  () async {
76
93
  final skip = _missingPrereq();
77
94
  if (skip != null) {
@@ -86,27 +103,15 @@ void main() {
86
103
  addTearDown, // deterministic auto-dispose even if an expect() throws
87
104
  );
88
105
 
89
- // Drive the REAL law: a create directive + a declared query — the same ops a generated client emits.
90
- final outcome = await harness.bridge.offerCreates(
106
+ await harness.assertDomainInstalled(
107
+ workspace: harness.workspace,
108
+ domain: 'guestbook',
109
+ );
110
+ await harness.reopenWorkspaceCustody(harness.workspace);
111
+ await harness.assertDomainInstalled(
112
+ workspace: harness.workspace,
91
113
  domain: 'guestbook',
92
- directiveId: 'signGuestbook',
93
- payload: {
94
- 'author': 'harness',
95
- 'message': 'offline fixture green',
96
- 'mood': 'happy',
97
- 'signedAt': DateTime.now().toUtc().toIso8601String(),
98
- },
99
114
  );
100
- expect(outcome.created, hasLength(1));
101
- expect(outcome.created.single.type, 'guestbook.GuestbookEntry');
102
-
103
- final rows =
104
- await harness.bridge.query('entriesByAuthor', {'author': 'harness'});
105
- expect(rows, hasLength(1));
106
- expect(rows.first['id'], outcome.created.single.id);
107
- expect((rows.first['data'] as Map)['message'], 'offline fixture green');
108
- expect((rows.first['data'] as Map)['moodEmoji'],
109
- '😊'); // proves the law's plan ran, not a stub
110
115
 
111
116
  stdout.writeln('offline harness green: ws=${harness.workspace}');
112
117
  }, timeout: const Timeout(Duration(minutes: 2)));
@@ -125,114 +130,85 @@ void main() {
125
130
  runtimeDir: _runtimeDir,
126
131
  registerTearDown: addTearDown,
127
132
  );
128
- // harness.bridge is a SESSION HANDLE — pinned ops work as before…
129
- expect(harness.bridge, isA<NomosSession>());
130
- expect(await harness.bridge.query('entriesByAuthor', {'author': 'x'}),
131
- isEmpty);
132
- // …but the RAW PLANE holds the decoy too, so an op that forgot its session refuses HERE —
133
- // exactly the refusal a real multi-workspace app would hit (never a silent sole-session guess).
134
- final plane = (harness.bridge as NomosSession).parent;
135
- await expectLater(
136
- plane.query('entriesByAuthor', {'author': 'x'}),
137
- throwsA(predicate((e) => '$e'.contains('multiple sessions'))),
138
- );
133
+ // harness.reception is a SESSION HANDLE and the raw plane also holds the decoy.
134
+ expect(harness.reception, isA<NomosSession>());
135
+ final plane = (harness.reception as NomosSession).parent;
136
+ expect(plane.boundSessions, containsAll(['test-ws', '__nomos-decoy']));
139
137
  }, timeout: const Timeout(Duration(minutes: 2)));
140
138
 
141
139
  test(
142
- 'DX: ephemeral emits phase-progress diagnostics (install → runner → ready → connect)',
140
+ 'CLUSTER: many workspaces share one Peer Office and keep separate Git custody',
143
141
  () async {
144
142
  final skip = _missingPrereq();
145
143
  if (skip != null) {
146
144
  markTestSkipped(skip);
147
145
  return;
148
146
  }
149
- final lines = <String>[];
150
- final harness = await NomosTestHarness.ephemeral(
151
- lawUsdzPaths: [_guestbookLawUsdz],
147
+ final harness = await NomosTestHarness.cluster(
148
+ workspaces: {
149
+ 'guestbook-a': [_guestbookLawUsdz],
150
+ 'guestbook-b': [_guestbookLawUsdz],
151
+ },
152
152
  nodeEntry: _repoEntry,
153
153
  runtimeDir: _runtimeDir,
154
154
  registerTearDown: addTearDown,
155
- onDiagnostic: lines.add,
156
155
  );
157
- final joined = lines.join('\n');
158
- // the four named phases are surfaced so an AppSim can tell install vs runner-wait vs connect-block.
159
- expect(joined, contains('phase 1/4: offline law install'));
160
- expect(joined, contains('phase 2/4: spawn the node runner'));
161
- expect(joined, contains('phase 2/4: spawn the node runner done'));
162
- expect(joined, contains('phase 3/4: runner ready'));
163
- expect(joined, contains('phase 4/4: connect session'));
164
- expect(joined, contains('phase 4/4: connect session "test-ws" done'));
165
- expect(joined, contains('ready — session "test-ws" connected'));
166
- expect(harness.bridge, isNotNull);
156
+ expect(harness.sessions, ['guestbook-a', 'guestbook-b']);
157
+ await harness.assertDomainInstalled(
158
+ workspace: 'guestbook-a',
159
+ domain: 'guestbook',
160
+ );
161
+ await harness.assertDomainInstalled(
162
+ workspace: 'guestbook-b',
163
+ domain: 'guestbook',
164
+ );
165
+ expect(
166
+ harness.session('guestbook-a'), isNot(harness.session('guestbook-b')));
167
167
  }, timeout: const Timeout(Duration(minutes: 2)));
168
168
 
169
- test('OFFLINE: dispose is idempotent and removes the temp dir', () async {
169
+ test('DX: ephemeral emits canonical Peer Office and install progress',
170
+ () async {
170
171
  final skip = _missingPrereq();
171
172
  if (skip != null) {
172
173
  markTestSkipped(skip);
173
174
  return;
174
175
  }
176
+ final lines = <String>[];
175
177
  final harness = await NomosTestHarness.ephemeral(
176
178
  lawUsdzPaths: [_guestbookLawUsdz],
177
179
  nodeEntry: _repoEntry,
178
180
  runtimeDir: _runtimeDir,
179
- autoTearDown: false, // drive dispose explicitly to assert its determinism
181
+ registerTearDown: addTearDown,
182
+ onDiagnostic: lines.add,
180
183
  );
181
- // A read proves the holon is live before teardown.
182
- expect(await harness.bridge.query('entriesByAuthor', {'author': 'nobody'}),
183
- isEmpty);
184
-
185
- await harness.dispose();
186
- await harness
187
- .dispose(); // idempotent: the second call is a no-op, never throws
188
- // The bridge is dead after dispose — a call fails rather than silently reaching a leaked runner.
189
- await expectLater(harness.bridge.query('entriesByAuthor', {'author': 'x'}),
190
- throwsA(anything));
184
+ final joined = lines.join('\n');
185
+ expect(joined, contains('phase 1/2: open canonical test peer'));
186
+ expect(joined, contains('phase 2/2: create Git custody and install law'));
187
+ expect(joined, contains('ready — session "test-ws" connected'));
188
+ expect(harness.reception, isNotNull);
191
189
  }, timeout: const Timeout(Duration(minutes: 2)));
192
190
 
193
- test(
194
- 'OFFLINE: the `githolon harness-install` SUBCOMMAND lane installs law and drives a read',
195
- () async {
191
+ test('OFFLINE: dispose is idempotent and removes the temp dir', () async {
196
192
  final skip = _missingPrereq();
197
193
  if (skip != null) {
198
194
  markTestSkipped(skip);
199
195
  return;
200
196
  }
201
- if (!File(_cliDist).existsSync()) {
202
- markTestSkipped(
203
- 'build the CLI first: cd cli && npm run build (needs dist/cli.mjs)');
204
- return;
205
- }
206
- // FORCE the published subcommand lane via the installHelper seam: a githolon command line (here the
207
- // in-repo built bin) whose `harness-install` subcommand does the offline install a published tenant gets.
208
197
  final harness = await NomosTestHarness.ephemeral(
209
198
  lawUsdzPaths: [_guestbookLawUsdz],
210
199
  nodeEntry: _repoEntry,
211
200
  runtimeDir: _runtimeDir,
212
- installHelper:
213
- 'node $_cliDist', // NOT an existing .mjs file ⇒ the subcommand lane
214
- registerTearDown: addTearDown,
201
+ autoTearDown: false, // drive dispose explicitly to assert its determinism
215
202
  );
216
-
217
- final outcome = await harness.bridge.offerCreates(
203
+ await harness.assertDomainInstalled(
204
+ workspace: harness.workspace,
218
205
  domain: 'guestbook',
219
- directiveId: 'signGuestbook',
220
- payload: {
221
- 'author': 'subcommand',
222
- 'message': 'installed via githolon harness-install',
223
- 'mood': 'happy',
224
- 'signedAt': DateTime.now().toUtc().toIso8601String(),
225
- },
226
206
  );
227
- expect(outcome.created, hasLength(1));
228
- final rows =
229
- await harness.bridge.query('entriesByAuthor', {'author': 'subcommand'});
230
- expect(rows, hasLength(1));
231
- expect((rows.first['data'] as Map)['message'],
232
- 'installed via githolon harness-install');
233
- expect((rows.first['data'] as Map)['moodEmoji'],
234
- '😊'); // the law's plan ran through the subcommand-built snapshot
235
207
 
236
- stdout.writeln('subcommand harness green: ws=${harness.workspace}');
208
+ await harness.dispose();
209
+ await harness
210
+ .dispose(); // idempotent: the second call is a no-op, never throws
211
+ // The bridge is dead after dispose — a call fails rather than silently reaching a leaked runner.
212
+ await expectLater(harness.reception.authorKeygen(), throwsA(anything));
237
213
  }, timeout: const Timeout(Duration(minutes: 2)));
238
214
  }