@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.
- package/assets/operation-contract.wasm +0 -0
- package/check_package.mjs +12 -15
- package/compile_package.mjs +17 -18
- package/compile_proof.mjs +16 -1
- package/dart/CHANGELOG.md +76 -1
- package/dart/README.md +25 -2
- package/dart/lib/src/admission_decision.dart +41 -0
- package/dart/lib/src/application_framework_calls.dart +52 -2
- package/dart/lib/src/application_model_adoption.dart +187 -0
- package/dart/lib/src/application_status.dart +73 -0
- package/dart/lib/src/console_enrolment.dart +271 -0
- package/dart/lib/src/console_http_io.dart +26 -0
- package/dart/lib/src/console_http_stub.dart +7 -0
- package/dart/lib/src/dev_cloud.dart +69 -22
- package/dart/lib/src/generated_call.dart +83 -23
- package/dart/lib/src/kernel_primitives.dart +119 -0
- package/dart/lib/src/nms_conformance_runtime.dart +1 -1
- package/dart/lib/src/nms_generated_runtime.dart +32 -10
- package/dart/lib/src/nms_test_environment.dart +1 -1
- package/dart/lib/src/node_runner_transport.dart +41 -46
- package/dart/lib/src/observed_session_directory.dart +70 -0
- package/dart/lib/src/operation_receipts.dart +112 -0
- package/dart/lib/src/peer_application_streams.dart +103 -0
- package/dart/lib/src/reception/dart_reception.dart +4280 -0
- package/dart/lib/src/reception/event_desk.dart +211 -0
- package/dart/lib/src/reception/extremum_read.dart +46 -0
- package/dart/lib/src/reception/local_copies.dart +371 -0
- package/dart/lib/src/reception/peer_office_connection.dart +11 -0
- package/dart/lib/src/reception/progress_desk.dart +267 -0
- package/dart/lib/src/reception/reception_contract.dart +11 -0
- package/dart/lib/src/reception/reception_session.dart +284 -0
- package/dart/lib/src/reception/sync_diagnostics.dart +26 -0
- package/dart/lib/src/reception/workspace_preparation_report.dart +126 -0
- package/dart/lib/src/replica_readiness.dart +123 -0
- package/dart/lib/src/runner/node-entry.mjs +23905 -15774
- package/dart/lib/src/runtime_v2_gateway.dart +56 -3
- package/dart/lib/src/runtime_v2_transport.dart +544 -3
- package/dart/lib/src/session_bound.dart +200 -311
- package/dart/lib/src/signing_identity_kernel.dart +84 -0
- package/dart/lib/src/sync_status_projection.dart +548 -0
- package/dart/lib/src/sync_status_reducer.dart +170 -0
- package/dart/lib/src/test_harness.dart +809 -1153
- package/dart/lib/src/testing/test_custody.dart +66 -0
- package/dart/lib/src/testing/test_peer.dart +328 -0
- package/dart/lib/src/typed_watch_registry.dart +169 -0
- package/dart/lib/src/workspace_open_progress.dart +27 -0
- package/dart/pubspec.lock +24 -1
- package/dart/pubspec.yaml +7 -2
- package/dart/pubspec_overrides.yaml +2 -0
- package/dart/test/admission_decision_test.dart +48 -0
- package/dart/test/background_model_adoption_test.dart +228 -0
- package/dart/test/bundle_version_pairing_test.dart +4 -4
- package/dart/test/child_authentication_repair_test.dart +359 -0
- package/dart/test/cloud_bound_local_shell_test.dart +119 -0
- package/dart/test/console_enrolment_refusal_test.dart +52 -0
- package/dart/test/console_enrolment_test.dart +227 -0
- package/dart/test/errors_test.dart +5 -5
- package/dart/test/event_plane_seam_test.dart +50 -164
- package/dart/test/extremum_read_test.dart +58 -0
- package/dart/test/fixtures/wedge/wedge-entry.mjs +3 -21
- package/dart/test/generated_query_test.dart +32 -0
- package/dart/test/harness_phase_timeout_test.dart +18 -23
- package/dart/test/kernel_author_after_read_test.dart +242 -0
- package/dart/test/kernel_fence_test.dart +137 -0
- package/dart/test/local_copies_test.dart +222 -0
- package/dart/test/model_adoption_test.dart +198 -0
- package/dart/test/node_peer_local_custody_test.dart +153 -0
- package/dart/test/offer_authentication_proof_test.dart +352 -52
- package/dart/test/operation_compatibility_test.dart +151 -0
- package/dart/test/platform_credential_effect_test.dart +386 -0
- package/dart/test/receiving_group_open_test.dart +213 -0
- package/dart/test/reception/workspace_preparation_report_test.dart +103 -0
- package/dart/test/reception_span_trace_test.dart +23 -0
- package/dart/test/replica_readiness_test.dart +156 -0
- package/dart/test/runtime_projections_test.dart +92 -0
- package/dart/test/runtime_v2_gateway_test.dart +131 -0
- package/dart/test/signing_identity_kernel_test.dart +75 -0
- package/dart/test/sync_status_preparation_report_test.dart +29 -0
- package/dart/test/sync_status_reducer_test.dart +419 -0
- package/dart/test/test_harness_test.dart +71 -95
- package/dart/test/typed_query_watch_test.dart +170 -0
- package/dart/test/typed_watch_registry_test.dart +249 -0
- package/dart_lints/README.md +18 -0
- package/dart_lints/lib/nomos_lints.dart +70 -0
- package/dart_lints/pubspec.lock +580 -0
- package/dart_lints/pubspec.yaml +12 -0
- package/dart_lints/test/reference_identity_test.dart +75 -0
- package/dart_types/CHANGELOG.md +18 -0
- package/dart_types/lib/nomos_kernel_protocol.dart +21 -1
- package/dart_types/lib/nomos_types.dart +9 -1
- package/dart_types/lib/src/admission_decision.dart +37 -0
- package/dart_types/lib/src/aggregate_meta.dart +1 -1
- package/dart_types/lib/src/approved_model.dart +65 -0
- package/dart_types/lib/src/custody.dart +10 -18
- package/dart_types/lib/src/error_info.dart +4 -4
- package/dart_types/lib/src/errors.dart +29 -29
- package/dart_types/lib/src/foreign_ref.dart +62 -33
- package/dart_types/lib/src/local_work_rejected.dart +3 -3
- package/dart_types/lib/src/nomos_ref.dart +3 -1
- package/dart_types/lib/src/principal.dart +2 -2
- package/dart_types/lib/src/proto/google/protobuf/any.pb.dart +163 -0
- package/dart_types/lib/src/proto/google/protobuf/any.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pb.dart +137 -0
- package/dart_types/lib/src/proto/nomos/discovery/v1/workspace_placement.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +20721 -10433
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +555 -27
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +387 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pbenum.dart +46 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pb.dart +295 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/application_readiness_desk.pbenum.dart +139 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pb.dart +269 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/authentication_desk.pbenum.dart +103 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pb.dart +417 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/compatibility_desk.pbenum.dart +62 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +9968 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +133 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pb.dart +204 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/credentials_desk.pbenum.dart +54 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pb.dart +1550 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/custody_desk.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pb.dart +710 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/framework.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pb.dart +845 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/identity_desk.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pb.dart +198 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/interaction_desk.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pb.dart +369 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/law_desk.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +609 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pbenum.dart +65 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +358 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pbenum.dart +100 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +2231 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +271 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pb.dart +1286 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/placement_desk.pbenum.dart +11 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +636 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pbenum.dart +116 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +1637 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pbenum.dart +175 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pb.dart +1269 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/recovery_desk.pbenum.dart +129 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +2782 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pbenum.dart +97 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +2232 -8627
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +0 -358
- package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pb.dart +1639 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/sealing_desk.pbenum.dart +46 -0
- package/dart_types/lib/src/runtime_outcome.dart +133 -0
- package/dart_types/lib/src/subscriptions.dart +116 -23
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/extremum_subscriptions_test.dart +40 -0
- package/dart_types/test/foreign_ref_resolution_test.dart +75 -0
- package/dart_types/test/nomos_ref_address_test.dart +25 -0
- package/dart_types/test/ordered_subscriptions_test.dart +34 -0
- package/operation_contract_check.mjs +24 -0
- package/package.json +11 -6
- package/src/aggregate.ts +70 -10
- package/src/application_topology.ts +18 -11
- package/src/attested.ts +3 -0
- package/src/attested_core.ts +46 -3
- package/src/birth_meter_ensure.ts +19 -7
- package/src/births.ts +29 -4
- package/src/build_package.ts +140 -13
- package/src/business_birth.ts +181 -4
- package/src/codegen_call_proto.ts +1274 -0
- package/src/codegen_call_usda.ts +49 -6
- package/src/codegen_dart.ts +79 -44
- package/src/codegen_dart_client.ts +458 -800
- package/src/{codegen_dart_application.ts → codegen_dart_facade.ts} +331 -582
- package/src/codegen_dart_framework_calls.ts +320 -37
- package/src/codegen_dart_indexed_variant.ts +159 -0
- package/src/codegen_dart_scenario.ts +42 -13
- package/src/codegen_proof.ts +3 -3
- package/src/codegen_provider_dart.ts +5 -0
- package/src/codegen_query_proto.ts +158 -0
- package/src/codegen_refusal.ts +189 -0
- package/src/codegen_result_proto.ts +931 -0
- package/src/codegen_result_usda.ts +4 -0
- package/src/codegen_ts.ts +464 -616
- package/src/codegen_workspace_access.ts +105 -0
- package/src/compatibility_graph.ts +759 -0
- package/src/compatibility_usda.ts +264 -0
- package/src/compile_package_main.ts +1994 -1189
- package/src/custody_placement.ts +156 -0
- package/src/custody_placement_usda.ts +91 -0
- package/src/decl_shapes.ts +21 -2
- package/src/describe_model.ts +1 -0
- package/src/directive.ts +59 -2
- package/src/drivers.ts +51 -0
- package/src/engine_entry.ts +52 -8
- package/src/evolve_preview.ts +11 -4
- package/src/exists.ts +2 -0
- package/src/fields.ts +75 -11
- package/src/flow_gate.ts +2 -1
- package/src/framework/application_home.ts +170 -18
- package/src/framework/attested_receipts.ts +8 -3
- package/src/framework/auth_provider_law.ts +169 -9
- package/src/framework/birth_certificate_law.ts +4 -0
- package/src/framework/capability.ts +34 -3
- package/src/framework/child_authentication.ts +346 -0
- package/src/framework/console_governance.ts +295 -0
- package/src/framework/delegated_roles.ts +301 -0
- package/src/framework/domain_lifecycle.ts +26 -13
- package/src/framework/founding_genesis.ts +193 -0
- package/src/framework/framework_aggregates.ts +1 -1
- package/src/framework/genesis.ts +6 -1
- package/src/framework/governance_relations.ts +19 -0
- package/src/framework/home.ts +5 -734
- package/src/framework/legacy/child_auth_provider_epoch.ts +73 -0
- package/src/framework/model_authority_repair.ts +116 -0
- package/src/framework/model_authority_seed.ts +185 -0
- package/src/framework/model_transition.ts +211 -0
- package/src/framework/owned_custody.ts +1 -1
- package/src/framework/principal_enrollment.ts +302 -0
- package/src/framework/receiving_group.ts +857 -0
- package/src/framework/receiving_group_recovery.ts +314 -0
- package/src/framework/relations.ts +4 -8
- package/src/framework/signing_identity.ts +242 -95
- package/src/framework/source_roster.ts +11 -5
- package/src/framework/workspace_access.ts +149 -0
- package/src/framework/workspace_invariant.ts +73 -52
- package/src/framework/workspaces.ts +407 -40
- package/src/framework_call_definitions.ts +32 -3
- package/src/generated_client_bundle_boundary.ts +97 -0
- package/src/generated_client_names.ts +1 -0
- package/src/identity_usda.ts +27 -0
- package/src/index.ts +183 -12
- package/src/indexed_variant.ts +83 -0
- package/src/intent.ts +173 -45
- package/src/law_check.ts +8 -5
- package/src/law_dependency_index.ts +231 -0
- package/src/law_graph.ts +508 -71
- package/src/law_graph_usda.ts +170 -24
- package/src/law_identity.ts +119 -81
- package/src/law_source_map.ts +4 -1
- package/src/law_units.ts +498 -0
- package/src/law_value_type.ts +7 -3
- package/src/member_authentication.ts +302 -0
- package/src/null_meta.ts +1 -1
- package/src/openusd_compiled_layer.ts +132 -0
- package/src/operation_contract_check.ts +31 -0
- package/src/ordered_read.ts +2 -1
- package/src/package_config.ts +33 -12
- package/src/package_dependencies.ts +1 -0
- package/src/plan_source_slice.ts +30 -1
- package/src/protobuf_field_number.ts +36 -0
- package/src/provenance_wire.ts +6 -0
- package/src/query.ts +2 -1
- package/src/read.ts +50 -0
- package/src/relation.ts +4 -1
- package/src/relation_schema.ts +15 -2
- package/src/report.ts +2 -2
- package/src/sqlite_computation_bundle.ts +78 -0
- package/src/sqlite_computed.ts +102 -0
- package/src/sqlite_counts.ts +154 -0
- package/src/sqlite_layout.ts +823 -0
- package/src/sqlite_reads.ts +275 -0
- package/src/sqlite_samples.ts +185 -0
- package/src/stable_id_identity_bridge.ts +1 -1
- package/src/stable_ids_for_module.ts +16 -4
- package/src/test_call_usda.ts +6 -6
- package/src/usda_relationship_integrity.ts +20 -3
- package/src/usda_text.ts +21 -1
- package/src/value_object.ts +81 -11
- package/src/wire.ts +22 -0
- package/src/wire_encode.ts +227 -20
- package/src/workspace_sharding.ts +15 -6
- package/src/workspace_sharing.ts +51 -49
- package/src/zod_introspection.ts +2 -0
- package/compat_package.mjs +0 -46
- package/dart/lib/src/home.dart +0 -15
- package/dart/lib/src/runner/offline-install.mjs +0 -15367
- package/dart/lib/src/source_trust.dart +0 -51
- package/dart/lib/src/webview_bridge.dart +0 -6540
- package/dart/lib/src/workspace_name.dart +0 -30
- package/dart/test/acquire_session_credentials_test.dart +0 -150
- package/dart/test/acquire_session_test.dart +0 -534
- package/dart/test/adv_realm_interference_test.dart +0 -600
- package/dart/test/cloud_bound_topology_test.dart +0 -168
- package/dart/test/cluster_births_test.dart +0 -202
- package/dart/test/cluster_device_keyed_acceptance_test.dart +0 -105
- package/dart/test/cluster_local_autosync_test.dart +0 -123
- package/dart/test/cluster_scale_test.dart +0 -122
- package/dart/test/compat_test.dart +0 -61
- package/dart/test/dlq_session_surface_test.dart +0 -222
- package/dart/test/first_app_gauntlet_test.dart +0 -386
- package/dart/test/fixtures/wedge/fake-install.mjs +0 -11
- package/dart/test/four_layer_nested_topology_test.dart +0 -290
- package/dart/test/home_parity_test.dart +0 -13
- package/dart/test/local_sync_inert_test.dart +0 -107
- package/dart/test/node_runner_transport_test.dart +0 -78
- package/dart/test/parent_signer_recovery_test.dart +0 -155
- package/dart/test/residency_refusal_test.dart +0 -49
- package/dart/test/sealing_client_test.dart +0 -181
- package/dart/test/session_bound_test.dart +0 -285
- package/dart/test/stale_bundle_test.dart +0 -80
- package/dart/test/vo_read_typing_test.dart +0 -131
- package/dart/test/watch_after_offer_stall_test.dart +0 -100
- package/dart/test/webview_bridge_test.dart +0 -1930
- package/dart/test/workspace_name_test.dart +0 -24
- package/runtime/generated-application-host.js +0 -22860
- package/src/codegen_query_usda.ts +0 -214
- package/src/compat_manifest.ts +0 -26
- package/src/compat_package_main.ts +0 -224
- package/src/shape_compat.ts +0 -352
- package/src/usd_state.ts +0 -505
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for
|
|
1
|
+
// NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for app tests that run without a UI. One call
|
|
2
2
|
// installs a tenant's compiled law into a FRESH LOCAL application parent (no cloud contact, ever), performs
|
|
3
3
|
// the same receiving-group birth a production application performs, boots the REAL githolon (the
|
|
4
4
|
// byte-identical wasm, the real ledger, the real gate) over [NodeRunnerTransport], and hands
|
|
5
|
-
// out a [
|
|
5
|
+
// out a [DartReception] the generated Dart clients drive — the SAME surface the app UI uses — under
|
|
6
6
|
// `flutter test` / `dart test` / CI.
|
|
7
7
|
//
|
|
8
8
|
// ```dart
|
|
@@ -10,28 +10,20 @@
|
|
|
10
10
|
// lawUsdzPaths: ['build/guestbook.law.usdz'],
|
|
11
11
|
// registerTearDown: addTearDown, // auto-disposes when the test ends (see below)
|
|
12
12
|
// );
|
|
13
|
-
// final app = guestbookClient(harness.
|
|
13
|
+
// final app = guestbookClient(harness.reception); // the generated client — same one the app uses
|
|
14
14
|
// await app.signGuestbook(author: 'ada', message: 'hi', mood: 'happy', signedAt: ...);
|
|
15
15
|
// final rows = await app.entriesByAuthor(author: 'ada');
|
|
16
16
|
// await harness.dispose(); // DETERMINISTIC teardown (also auto-fired via registerTearDown)
|
|
17
17
|
// ```
|
|
18
18
|
//
|
|
19
|
-
// HOW IT STAYS OFFLINE.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// scenarios then birth their deterministic receiving group locally through the real declared birth.
|
|
24
|
-
// 2. the node runner (`node-entry.ts`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
|
|
25
|
-
// local cache (`~/.holon/runtime`, filled by any warm `githolon` run) and boots the holon from that
|
|
26
|
-
// snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
|
|
19
|
+
// HOW IT STAYS OFFLINE. One Node test Platform Shell opens the canonical Peer Office over a temporary
|
|
20
|
+
// filesystem Git custody directory. Workspace genesis and every law install cross Runtime v2 and the
|
|
21
|
+
// kernel's generated Protobuf ABI. There is no private installer, serialized repository snapshot, alternate
|
|
22
|
+
// engine, USDA parser, or generic command bridge in this harness.
|
|
27
23
|
//
|
|
28
24
|
// PREREQUISITES (documented so a failure is legible): `node` on PATH and the release-pinned runtime cache.
|
|
29
|
-
// The self-contained runner
|
|
25
|
+
// The self-contained runner ships inside nomos_client; the kernel executable does not. A cold
|
|
30
26
|
// runtime is acquired and content-addressed by the release tooling, then tests remain fully offline.
|
|
31
|
-
//
|
|
32
|
-
// PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
|
|
33
|
-
// runner. For a PUBLISHED harness this install belongs behind a `githolon` subcommand (the CLI already
|
|
34
|
-
// bundles the engine + this exact sequence in `proof_offline`); [installHelper] is the one seam to repoint.
|
|
35
27
|
@internal
|
|
36
28
|
library;
|
|
37
29
|
|
|
@@ -40,18 +32,28 @@ import 'package:meta/meta.dart' show internal;
|
|
|
40
32
|
import 'dart:convert';
|
|
41
33
|
import 'dart:io';
|
|
42
34
|
|
|
35
|
+
import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
|
|
36
|
+
|
|
43
37
|
import 'package:crypto/crypto.dart' show sha256;
|
|
44
38
|
import 'package:fixnum/fixnum.dart' show Int64;
|
|
45
39
|
import 'package:nomos_types/nomos_kernel_protocol.dart'
|
|
46
40
|
show BirthCertificate, SyncWorkspaceResponse;
|
|
47
|
-
import 'package:nomos_types/nomos_types.dart'
|
|
41
|
+
import 'package:nomos_types/nomos_types.dart'
|
|
42
|
+
show
|
|
43
|
+
Sha256Digest,
|
|
44
|
+
NomosRuntimeFailure,
|
|
45
|
+
NomosUnavailable,
|
|
46
|
+
NomosUnavailableReason;
|
|
48
47
|
import 'package:package_config/package_config.dart' show loadPackageConfigUri;
|
|
49
48
|
|
|
50
49
|
import 'dev_cloud.dart';
|
|
51
50
|
import 'application_framework_calls.dart';
|
|
52
51
|
import 'node_runner_transport.dart';
|
|
52
|
+
import 'runtime_v2_transport.dart';
|
|
53
53
|
import 'session_bound.dart' show DomainNotInstalledForSession;
|
|
54
|
-
import '
|
|
54
|
+
import 'reception/dart_reception.dart'
|
|
55
|
+
show DartReception, NomosBoundInteraction, NomosSession;
|
|
56
|
+
import 'testing/test_peer.dart' show NmsTestPeer;
|
|
55
57
|
|
|
56
58
|
enum _KernelRuntimeArtifact {
|
|
57
59
|
executable('holon.wasm'),
|
|
@@ -64,38 +66,17 @@ enum _KernelRuntimeArtifact {
|
|
|
64
66
|
final String? providerFileName;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
final
|
|
69
|
+
/// Opaque authenticated Git point captured from one isolated local test peer.
|
|
70
|
+
/// It carries no credentials and cannot be used with a production reception.
|
|
71
|
+
final class NomosTestFrontier {
|
|
72
|
+
const NomosTestFrontier._(
|
|
73
|
+
this._owner, this.workspace, this.head, this._offers);
|
|
74
|
+
final Object _owner;
|
|
75
|
+
final String workspace;
|
|
76
|
+
final String head;
|
|
77
|
+
final protocol.CustodyOfferBatchRequest _offers;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
/// Lower the generated protobuf certificate into the framework law's USDA-authored value shape. This is
|
|
76
|
-
/// deliberately not a second certificate type: callers cannot construct it independently, and every field
|
|
77
|
-
/// is read from the compiler-generated message.
|
|
78
|
-
Map<String, Object?> _birthCertificateLawValue(BirthCertificate certificate) =>
|
|
79
|
-
<String, Object?>{
|
|
80
|
-
'child': certificate.child,
|
|
81
|
-
'founder': certificate.founder,
|
|
82
|
-
'verified': certificate.verified,
|
|
83
|
-
'parentKey': base64Encode(certificate.parentKey),
|
|
84
|
-
'parentId': certificate.parentId,
|
|
85
|
-
'epoch': certificate.epoch.toInt(),
|
|
86
|
-
'signature': base64Encode(certificate.signature),
|
|
87
|
-
if (certificate.hasVerifiedVia()) 'verifiedVia': certificate.verifiedVia,
|
|
88
|
-
if (certificate.hasIdpToken()) 'idpToken': certificate.idpToken,
|
|
89
|
-
if (certificate.hasCapturedClock())
|
|
90
|
-
'capturedClock': certificate.capturedClock.toInt(),
|
|
91
|
-
if (certificate.hasAuthProviderIssuer())
|
|
92
|
-
'authProviderIssuer': certificate.authProviderIssuer,
|
|
93
|
-
if (certificate.hasAuthProviderJwks())
|
|
94
|
-
'authProviderJwks': certificate.authProviderJwks,
|
|
95
|
-
if (certificate.hasAuthProviderAudience())
|
|
96
|
-
'authProviderAudience': certificate.authProviderAudience,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
80
|
/// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
|
|
100
81
|
/// move together. Generated scenarios must not combine those source-generated laws with a recently-written
|
|
101
82
|
/// but older global runtime cache. Published packages have no release/ directory and keep the normal
|
|
@@ -105,9 +86,8 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
105
86
|
Directory? root;
|
|
106
87
|
for (var depth = 0; depth < 12; depth++) {
|
|
107
88
|
if (File('${cursor.path}/release/kernel-candidate.usda').existsSync() &&
|
|
108
|
-
File(
|
|
109
|
-
|
|
110
|
-
).existsSync()) {
|
|
89
|
+
File('${cursor.path}/cloud/cloudflare-shell/vendor/holon_bytes.wasmbin')
|
|
90
|
+
.existsSync()) {
|
|
111
91
|
root = cursor;
|
|
112
92
|
break;
|
|
113
93
|
}
|
|
@@ -156,19 +136,12 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
156
136
|
}
|
|
157
137
|
|
|
158
138
|
final productionExpected = await candidateField('production-sha256');
|
|
159
|
-
final testMintExpected = await candidateField('test-mint-sha256');
|
|
160
139
|
final production = File(
|
|
161
140
|
'${sourceRoot.path}/cloud/cloudflare-shell/vendor/holon_bytes.wasmbin',
|
|
162
141
|
);
|
|
163
|
-
final testMint = File(
|
|
164
|
-
'${sourceRoot.path}/cloud/dev-server/kernel-testmint.wasm',
|
|
165
|
-
);
|
|
166
142
|
final productionRepository = File(
|
|
167
143
|
'${sourceRoot.path}/cloud/cloudflare-shell/runtime/kernel.repository.xml',
|
|
168
144
|
);
|
|
169
|
-
final testMintRepository = File(
|
|
170
|
-
'${sourceRoot.path}/cloud/cloudflare-shell/runtime/kernel-testmint.repository.xml',
|
|
171
|
-
);
|
|
172
145
|
final bootstrap = File(
|
|
173
146
|
'${sourceRoot.path}/cloud/cloudflare-shell/domains/build/bootstrap/domain_bootstrap.law.usdz',
|
|
174
147
|
);
|
|
@@ -177,9 +150,7 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
177
150
|
);
|
|
178
151
|
for (final file in [
|
|
179
152
|
production,
|
|
180
|
-
testMint,
|
|
181
153
|
productionRepository,
|
|
182
|
-
testMintRepository,
|
|
183
154
|
bootstrap,
|
|
184
155
|
nomos,
|
|
185
156
|
]) {
|
|
@@ -190,19 +161,15 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
190
161
|
}
|
|
191
162
|
}
|
|
192
163
|
final productionBytes = await production.readAsBytes();
|
|
193
|
-
final testMintBytes = await testMint.readAsBytes();
|
|
194
164
|
final productionRepositoryBytes = await productionRepository.readAsBytes();
|
|
195
|
-
final testMintRepositoryBytes = await testMintRepository.readAsBytes();
|
|
196
165
|
final productionActual = sha256.convert(productionBytes).toString();
|
|
197
|
-
final testMintActual = sha256.convert(testMintBytes).toString();
|
|
198
166
|
String short(String value) =>
|
|
199
167
|
value.length <= 12 ? value : value.substring(0, 12);
|
|
200
|
-
if (productionActual != productionExpected
|
|
201
|
-
testMintActual != testMintExpected) {
|
|
168
|
+
if (productionActual != productionExpected) {
|
|
202
169
|
throw NomosTestHarnessException(
|
|
203
170
|
'source release candidate kernel mismatch: release/kernel-candidate.usda '
|
|
204
|
-
'expects ${short(productionExpected)}
|
|
205
|
-
'${short(productionActual)}
|
|
171
|
+
'expects ${short(productionExpected)}, but the staged artifact is '
|
|
172
|
+
'${short(productionActual)} — run the kernel candidate preparation before testing',
|
|
206
173
|
);
|
|
207
174
|
}
|
|
208
175
|
final bootstrapBytes = await bootstrap.readAsBytes();
|
|
@@ -210,9 +177,8 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
210
177
|
final bundleIdentity = sha256
|
|
211
178
|
.convert(
|
|
212
179
|
utf8.encode(
|
|
213
|
-
'$productionActual
|
|
180
|
+
'$productionActual:'
|
|
214
181
|
'${sha256.convert(productionRepositoryBytes)}:'
|
|
215
|
-
'${sha256.convert(testMintRepositoryBytes)}:'
|
|
216
182
|
'${sha256.convert(bootstrapBytes)}:'
|
|
217
183
|
'${sha256.convert(nomosBytes)}',
|
|
218
184
|
),
|
|
@@ -242,9 +208,7 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
|
|
|
242
208
|
}
|
|
243
209
|
|
|
244
210
|
await publish('holon.wasm', productionBytes);
|
|
245
|
-
await publish('holon-testmint.wasm', testMintBytes);
|
|
246
211
|
await publish('kernel.repository.xml', productionRepositoryBytes);
|
|
247
|
-
await publish('kernel-testmint.repository.xml', testMintRepositoryBytes);
|
|
248
212
|
await publish('bootstrap.law.usdz', bootstrapBytes);
|
|
249
213
|
await publish('nomos.law.usdz', nomosBytes);
|
|
250
214
|
return runtime.path;
|
|
@@ -262,25 +226,21 @@ String? nmsPermanentConvergenceFailure(SyncWorkspaceResponse result) {
|
|
|
262
226
|
'push=${result.hasPushError() ? result.pushError.message : "none"}';
|
|
263
227
|
}
|
|
264
228
|
|
|
265
|
-
///
|
|
266
|
-
///
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
required this.runtimeDir,
|
|
281
|
-
required this.clientId,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
229
|
+
/// A missing frontier or retryable transport/discovery failure is not a
|
|
230
|
+
/// successful remote convergence merely because its counters default to zero.
|
|
231
|
+
bool nmsRemoteConvergenceSettled(SyncWorkspaceResponse result) =>
|
|
232
|
+
result.hasCurrentHead() &&
|
|
233
|
+
result.currentHead.isNotEmpty &&
|
|
234
|
+
!result.localOnly &&
|
|
235
|
+
!result.pushFailed &&
|
|
236
|
+
!result.stillAhead &&
|
|
237
|
+
!result.offline &&
|
|
238
|
+
!result.reconnectRequired &&
|
|
239
|
+
!result.directOfferDeferred &&
|
|
240
|
+
!result.hasPushError() &&
|
|
241
|
+
!result.hasDiscoveryError() &&
|
|
242
|
+
result.deferred == 0 &&
|
|
243
|
+
nmsPermanentConvergenceFailure(result) == null;
|
|
284
244
|
|
|
285
245
|
class NomosTestHarnessException implements Exception {
|
|
286
246
|
final String message;
|
|
@@ -292,7 +252,7 @@ class NomosTestHarnessException implements Exception {
|
|
|
292
252
|
/// A TYPED per-phase deadline breach — the "NO SILENT HANGS" guarantee: every lifecycle await in
|
|
293
253
|
/// [NomosTestHarness] (install, runner spawn, ready, connect, keygen, keyed reconnect, seed, dispose)
|
|
294
254
|
/// runs under a generous deadline and, instead of wedging forever with no output, throws THIS —
|
|
295
|
-
/// naming the phase, the workspace, how long it waited, the
|
|
255
|
+
/// naming the phase, the workspace, how long it waited, the reception op it was awaiting, and the
|
|
296
256
|
/// likely causes where they are known. Subclasses [NomosTestHarnessException] so existing
|
|
297
257
|
/// `on NomosTestHarnessException` handling keeps working. Tune deadlines with the harness's
|
|
298
258
|
/// `phaseTimeout` parameter (it overrides EVERY phase's default when set).
|
|
@@ -306,7 +266,7 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
|
|
|
306
266
|
/// How long the harness waited before giving up.
|
|
307
267
|
final Duration elapsed;
|
|
308
268
|
|
|
309
|
-
/// What the harness was awaiting (the
|
|
269
|
+
/// What the harness was awaiting (the reception op / subprocess), e.g. `connect`, `authorKeygen`.
|
|
310
270
|
final String awaiting;
|
|
311
271
|
|
|
312
272
|
/// Likely causes, where the harness knows them (mirrors DomainNotInstalledForSession.possibleCauses).
|
|
@@ -332,15 +292,16 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
|
|
|
332
292
|
const String _decoyWorkspace = '__nomos-decoy';
|
|
333
293
|
|
|
334
294
|
class _CloudRestartLane {
|
|
335
|
-
|
|
295
|
+
_CloudRestartLane({
|
|
336
296
|
required this.entry,
|
|
337
297
|
required this.node,
|
|
338
298
|
required this.cloud,
|
|
339
299
|
required this.authToken,
|
|
340
300
|
required this.authorSecret,
|
|
341
301
|
required this.clientId,
|
|
302
|
+
required this.custodyDirectory,
|
|
342
303
|
this.resolveFrom,
|
|
343
|
-
this.
|
|
304
|
+
this.arguments = const <String>[],
|
|
344
305
|
});
|
|
345
306
|
|
|
346
307
|
final String entry;
|
|
@@ -350,15 +311,16 @@ class _CloudRestartLane {
|
|
|
350
311
|
final String authorSecret;
|
|
351
312
|
final String clientId;
|
|
352
313
|
final String? resolveFrom;
|
|
353
|
-
final
|
|
314
|
+
final List<String> arguments;
|
|
315
|
+
String custodyDirectory;
|
|
354
316
|
}
|
|
355
317
|
|
|
356
318
|
class NomosTestHarness {
|
|
357
|
-
/// The connected
|
|
319
|
+
/// The connected reception — hand it to the generated typed clients (the same surface the app UI uses).
|
|
358
320
|
/// For [ephemeral] this is a [NomosSession] HANDLE pinned to [workspace] (the plane underneath also
|
|
359
321
|
/// holds a decoy session, so unpinned plane-level calls refuse the way they would in a real
|
|
360
322
|
/// multi-workspace app); for [cluster] it is the shared plane — name sessions via [session].
|
|
361
|
-
|
|
323
|
+
DartReception reception;
|
|
362
324
|
|
|
363
325
|
/// The local workspace name the law was installed into.
|
|
364
326
|
final String workspace;
|
|
@@ -387,15 +349,21 @@ class NomosTestHarness {
|
|
|
387
349
|
/// package's domainHash — the `lawHash` a `.births()` recipe names for the child's law install.
|
|
388
350
|
final Map<String, List<String>> _lawHashes;
|
|
389
351
|
|
|
352
|
+
/// Domain keys reported by the kernel while inspecting each installed law package.
|
|
353
|
+
final Map<String, Set<String>> _domainKeys;
|
|
354
|
+
|
|
390
355
|
/// The tenant law hashes installed into [workspace] (deploy order); empty if unknown.
|
|
391
356
|
List<String> lawHashes(String workspace) =>
|
|
392
357
|
List.unmodifiable(_lawHashes[workspace] ?? const []);
|
|
393
358
|
|
|
394
|
-
///
|
|
395
|
-
|
|
359
|
+
/// Present for local harnesses so a later scenario workspace uses the same
|
|
360
|
+
/// canonical Peer Office and real Git custody, rather than another installer.
|
|
361
|
+
final NmsTestPeer? _testPeer;
|
|
396
362
|
|
|
397
|
-
///
|
|
398
|
-
|
|
363
|
+
/// Sessions used only to establish the test application. They share the
|
|
364
|
+
/// kernel plane while setup runs, but are not part of the application's
|
|
365
|
+
/// durable working set and must never be reopened as though they were.
|
|
366
|
+
final Set<String> _setupSessions;
|
|
399
367
|
|
|
400
368
|
/// When set, overrides EVERY phase's default deadline (see [HarnessPhaseTimeout]).
|
|
401
369
|
final Duration? _phaseTimeout;
|
|
@@ -424,7 +392,7 @@ class NomosTestHarness {
|
|
|
424
392
|
}
|
|
425
393
|
|
|
426
394
|
NomosTestHarness._(
|
|
427
|
-
this.
|
|
395
|
+
this.reception,
|
|
428
396
|
this.workspace,
|
|
429
397
|
this._transport,
|
|
430
398
|
this._workdir,
|
|
@@ -434,8 +402,9 @@ class NomosTestHarness {
|
|
|
434
402
|
Map<String, String> deviceSecrets = const {},
|
|
435
403
|
this.frameworkHash,
|
|
436
404
|
Map<String, List<String>> lawHashes = const {},
|
|
437
|
-
Map<String, String
|
|
438
|
-
|
|
405
|
+
Map<String, Set<String>> domainKeys = const {},
|
|
406
|
+
NmsTestPeer? testPeer,
|
|
407
|
+
Set<String> setupSessions = const {},
|
|
439
408
|
Duration? phaseTimeout,
|
|
440
409
|
_CloudRestartLane? cloudRestartLane,
|
|
441
410
|
this.testCloud,
|
|
@@ -446,8 +415,9 @@ class NomosTestHarness {
|
|
|
446
415
|
_devicePublics = devicePublics,
|
|
447
416
|
_deviceSecrets = deviceSecrets,
|
|
448
417
|
_lawHashes = Map.of(lawHashes),
|
|
449
|
-
|
|
450
|
-
|
|
418
|
+
_domainKeys = Map.of(domainKeys),
|
|
419
|
+
_testPeer = testPeer,
|
|
420
|
+
_setupSessions = Set.unmodifiable(setupSessions),
|
|
451
421
|
_phaseTimeout = phaseTimeout,
|
|
452
422
|
_cloudRestartLane = cloudRestartLane;
|
|
453
423
|
|
|
@@ -476,18 +446,26 @@ class NomosTestHarness {
|
|
|
476
446
|
final where = workspace != null ? ' "$workspace"' : '';
|
|
477
447
|
log('phase $phase$where (≤${timeout.inSeconds}s) …');
|
|
478
448
|
final sw = Stopwatch()..start();
|
|
479
|
-
|
|
480
|
-
timeout
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
449
|
+
try {
|
|
450
|
+
final r = await run().timeout(
|
|
451
|
+
timeout,
|
|
452
|
+
onTimeout: () => throw HarnessPhaseTimeout(
|
|
453
|
+
phase: phase,
|
|
454
|
+
workspace: workspace,
|
|
455
|
+
elapsed: timeout,
|
|
456
|
+
awaiting: awaiting,
|
|
457
|
+
likelyCauses: likelyCauses,
|
|
458
|
+
),
|
|
459
|
+
);
|
|
460
|
+
log('phase $phase$where done (${sw.elapsedMilliseconds}ms)');
|
|
461
|
+
return r;
|
|
462
|
+
} on NomosRuntimeFailure catch (failure) {
|
|
463
|
+
// The presentation-safe exception deliberately omits diagnostic prose. Test fixtures
|
|
464
|
+
// still need the runtime's actual reason, without changing production UI rendering.
|
|
465
|
+
log('phase $phase$where failed (${sw.elapsedMilliseconds}ms): '
|
|
466
|
+
'${failure.code}: ${failure.message}');
|
|
467
|
+
rethrow;
|
|
468
|
+
}
|
|
491
469
|
}
|
|
492
470
|
|
|
493
471
|
/// The base64 device PUBLIC key a `deviceKeyed` [cluster] session was opened with (null otherwise). Its
|
|
@@ -504,76 +482,14 @@ class NomosTestHarness {
|
|
|
504
482
|
|
|
505
483
|
/// The base64 device SECRET a session was opened with (TEST HARNESS ONLY — the harness IS the device;
|
|
506
484
|
/// a real app keeps this in secure storage). Sign writes to locally-born children with it, e.g.
|
|
507
|
-
/// `
|
|
485
|
+
/// `reception.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
|
|
508
486
|
String? deviceSecret(String workspace) => _deviceSecrets[workspace];
|
|
509
487
|
|
|
510
|
-
/// Framework-private scenario setup for a business intent guarded by a delegated role.
|
|
511
|
-
/// Generated `scenario.given(person).mayX()` methods are the only caller: they name the
|
|
512
|
-
/// business permission while this harness mints and admits the real parent-rooted delegation.
|
|
513
|
-
/// Tenant tests never handle relation names, certificate bytes, signer secrets, or raw offers.
|
|
514
|
-
Future<void> nomosInternalAllowDelegatedScenarioAction({
|
|
515
|
-
required String domain,
|
|
516
|
-
required String acceptanceIntent,
|
|
517
|
-
required String relation,
|
|
518
|
-
required String subject,
|
|
519
|
-
required String subjectField,
|
|
520
|
-
required String certificateField,
|
|
521
|
-
Map<String, Object?> facts = const {},
|
|
522
|
-
}) async {
|
|
523
|
-
final secret = _deviceSecrets[workspace];
|
|
524
|
-
if (secret == null || secret.isEmpty) {
|
|
525
|
-
throw NomosTestHarnessException(
|
|
526
|
-
'the generated scenario cannot establish its business permission because the test device secret is unavailable',
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
final rows = await session(
|
|
530
|
-
workspace,
|
|
531
|
-
).queryById('birth-attestation:self', null, subject);
|
|
532
|
-
if (rows.length != 1) {
|
|
533
|
-
throw NomosTestHarnessException(
|
|
534
|
-
'the generated scenario expected one verified birth attestation before establishing business permission; found ${rows.length}',
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
final row = rows.single;
|
|
538
|
-
final data = row['data'] is Map
|
|
539
|
-
? (row['data'] as Map).cast<String, Object?>()
|
|
540
|
-
: row.cast<String, Object?>();
|
|
541
|
-
final rawChain = data['certChain'];
|
|
542
|
-
final decodedChain = rawChain is String ? jsonDecode(rawChain) : rawChain;
|
|
543
|
-
final links = decodedChain is Map && decodedChain['links'] is List
|
|
544
|
-
? List<Object?>.from(decodedChain['links'] as List)
|
|
545
|
-
: const <Object?>[];
|
|
546
|
-
final issued = await session(workspace).issueDelegationCert(
|
|
547
|
-
signerSecret: secret,
|
|
548
|
-
grantorId: (data['parentId'] ?? workspace).toString(),
|
|
549
|
-
relation: relation,
|
|
550
|
-
subject: subject,
|
|
551
|
-
certChain: links,
|
|
552
|
-
);
|
|
553
|
-
final certificate = issued['certString']?.toString();
|
|
554
|
-
if (certificate == null || certificate.isEmpty) {
|
|
555
|
-
throw NomosTestHarnessException(
|
|
556
|
-
'the generated scenario could not mint the parent-rooted business delegation',
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
await session(workspace).offerOutcome(
|
|
560
|
-
domain: domain,
|
|
561
|
-
directiveId: acceptanceIntent,
|
|
562
|
-
actor: subject,
|
|
563
|
-
authorSecret: secret,
|
|
564
|
-
payload: <String, Object?>{
|
|
565
|
-
...facts,
|
|
566
|
-
subjectField: subject,
|
|
567
|
-
certificateField: certificate,
|
|
568
|
-
},
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
488
|
/// The workspace names on this device's local plane (one for [ephemeral], N for [cluster]).
|
|
573
489
|
List<String> get sessions => List.unmodifiable(_sessionNames);
|
|
574
490
|
|
|
575
491
|
/// A [NomosSession] handle pinned to one connected [workspace] — hand it to a generated typed client
|
|
576
|
-
/// (`EstateClient(harness.session('estate'))`). A THIN delegate to the production [
|
|
492
|
+
/// (`EstateClient(harness.session('estate'))`). A THIN delegate to the production [DartReception.session]:
|
|
577
493
|
/// the harness owns no session logic, it just installs the laws + opens the sessions on the one plane.
|
|
578
494
|
NomosSession session(String workspace) {
|
|
579
495
|
if (!_sessionNames.contains(workspace)) {
|
|
@@ -581,23 +497,18 @@ class NomosTestHarness {
|
|
|
581
497
|
'no session "$workspace" on this harness — connected sessions: $_sessionNames',
|
|
582
498
|
);
|
|
583
499
|
}
|
|
584
|
-
return
|
|
500
|
+
return reception.session(workspace);
|
|
585
501
|
}
|
|
586
502
|
|
|
587
|
-
/// Install [lawUsdzPaths] into a fresh LOCAL workspace and boot a
|
|
503
|
+
/// Install [lawUsdzPaths] into a fresh LOCAL workspace and boot a reception over it — NO cloud contact.
|
|
588
504
|
///
|
|
589
505
|
/// [lawUsdzPaths] — the `build/<name>.law.usdz` files `nomos-compile` emits (installed in order;
|
|
590
506
|
/// framework/platform law before a domain that depends on it).
|
|
591
507
|
/// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
|
|
592
|
-
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon
|
|
508
|
+
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` +
|
|
593
509
|
/// `bootstrap.law.usdz` + `nomos.law.usdz`. The harness owns a throwaway test root; app code never handles its key material.
|
|
594
|
-
/// [nodeEntry] — path to
|
|
595
|
-
/// `
|
|
596
|
-
/// [installHelper] — the offline law-install lane. An EXISTING `.ts` or `.mjs` file is run as a node script (the
|
|
597
|
-
/// in-repo `offline-install.ts`, the default when it exists beside [nodeEntry]); anything else is a
|
|
598
|
-
/// githolon COMMAND LINE whose `harness-install` subcommand is driven (the default `npx githolon` when the
|
|
599
|
-
/// in-repo helper is absent — a published tenant needs nothing but `githolon`). E.g. force the subcommand
|
|
600
|
-
/// with `installHelper: 'node /abs/cli/dist/cli.mjs'`.
|
|
510
|
+
/// [nodeEntry] — path to the bundled test Platform Shell; normally resolved
|
|
511
|
+
/// from `package:nomos_client/src/runner/node-entry.mjs` (the stdio node peer, given --custody).
|
|
601
512
|
/// [registerTearDown] — pass the test framework's `addTearDown` to auto-[dispose] when the test ends
|
|
602
513
|
/// (feature-detecting `package:test` from a library is not clean in Dart, so this is explicit and safe).
|
|
603
514
|
/// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
|
|
@@ -606,11 +517,15 @@ class NomosTestHarness {
|
|
|
606
517
|
NmsApplicationFrameworkCalls? applicationFrameworkCalls,
|
|
607
518
|
int? applicationMaxChildWorkspacesPerUser,
|
|
608
519
|
String? ownerSubject,
|
|
609
|
-
|
|
520
|
+
|
|
521
|
+
/// The raw law-focused lane's workspace. It is REFUSED alongside
|
|
522
|
+
/// [ownerSubject]: naming an owner declares the application-home lane, whose
|
|
523
|
+
/// workspace is the office's receiving-group address, not a name a caller
|
|
524
|
+
/// (or this harness) derives.
|
|
525
|
+
String? workspace,
|
|
610
526
|
String clientId = 'test-runner',
|
|
611
527
|
String? runtimeDir,
|
|
612
528
|
String? nodeEntry,
|
|
613
|
-
String? installHelper,
|
|
614
529
|
String? resolveFrom,
|
|
615
530
|
String node = 'node',
|
|
616
531
|
bool autoTearDown = true,
|
|
@@ -627,33 +542,24 @@ class NomosTestHarness {
|
|
|
627
542
|
);
|
|
628
543
|
}
|
|
629
544
|
final entry = nodeEntry ??
|
|
630
|
-
await
|
|
545
|
+
await _resolveTestEntry(node, resolveFrom ?? Directory.current.path);
|
|
631
546
|
final effectiveRuntimeDir = runtimeDir ??
|
|
632
547
|
Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
|
|
633
548
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
634
549
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
635
|
-
await _sourceCandidateRuntime(entry)
|
|
636
|
-
|
|
637
|
-
// `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
|
|
638
|
-
// bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
|
|
639
|
-
// • an EXISTING `.ts`/`.mjs` file ⇒ run it as a node script (the in-repo default when present);
|
|
640
|
-
// • anything else ⇒ treat it as a githolon COMMAND LINE and drive `harness-install`
|
|
641
|
-
// (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
|
|
642
|
-
// e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
|
|
643
|
-
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
644
|
-
final resolvedHelper = installHelper ??
|
|
645
|
-
(File(adjacentInstaller).existsSync()
|
|
646
|
-
? adjacentInstaller
|
|
647
|
-
: '${File(entry).parent.path}/offline-install.ts');
|
|
648
|
-
final useInRepoScript =
|
|
649
|
-
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
650
|
-
File(resolvedHelper).existsSync();
|
|
651
|
-
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
652
|
-
|
|
550
|
+
await _sourceCandidateRuntime(entry) ??
|
|
551
|
+
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
653
552
|
final owner = ownerSubject?.trim();
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
553
|
+
// THE CALLER DECLARES ITS LANE. Naming an owner IS the application-home lane;
|
|
554
|
+
// the harness no longer re-derives a receiving-group name to recognise one by
|
|
555
|
+
// comparison. The office answers that name once the peer is up.
|
|
556
|
+
final birthsApplicationHome = owner != null && owner.isNotEmpty;
|
|
557
|
+
if (birthsApplicationHome && workspace != null) {
|
|
558
|
+
throw NomosTestHarnessException(
|
|
559
|
+
'ownerSubject declares the application-home lane, whose workspace is the '
|
|
560
|
+
'office\'s receiving-group address for that owner — do not name `workspace` too',
|
|
561
|
+
);
|
|
562
|
+
}
|
|
657
563
|
if (birthsApplicationHome && applicationFrameworkCalls == null) {
|
|
658
564
|
throw NomosTestHarnessException(
|
|
659
565
|
'a generated application scenario must pass its compiler-generated framework calls; '
|
|
@@ -666,101 +572,71 @@ class NomosTestHarness {
|
|
|
666
572
|
// birth machinery production uses. Raw/law-focused harness callers keep their explicitly named workspace.
|
|
667
573
|
final installWorkspace = birthsApplicationHome
|
|
668
574
|
? 'nms-app-${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}'
|
|
669
|
-
: workspace;
|
|
575
|
+
: (workspace ?? 'test-ws');
|
|
670
576
|
|
|
671
|
-
|
|
672
|
-
// so dispose() leaves nothing behind.
|
|
673
|
-
final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
|
|
674
|
-
// Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
|
|
577
|
+
Directory? workdir;
|
|
675
578
|
NodeRunnerTransport? transport;
|
|
676
579
|
final phase = (String m) {
|
|
677
580
|
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
678
581
|
};
|
|
679
582
|
try {
|
|
680
|
-
final
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
workspace: installWorkspace,
|
|
684
|
-
awaiting:
|
|
685
|
-
useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
|
|
686
|
-
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
687
|
-
onDiagnostic: onDiagnostic,
|
|
688
|
-
likelyCauses: const [
|
|
689
|
-
'a cold npx download of githolon (pre-warm with any `githolon` run)',
|
|
690
|
-
'a very large law package folding through the offline gate — raise phaseTimeout',
|
|
691
|
-
],
|
|
692
|
-
run: () => _runInstall(
|
|
693
|
-
node: node,
|
|
694
|
-
useInRepoScript: useInRepoScript,
|
|
695
|
-
inRepoScript: resolvedHelper,
|
|
696
|
-
githolonCmd: githolonCmd,
|
|
697
|
-
lawUsdzPaths: lawUsdzPaths,
|
|
698
|
-
workspace: installWorkspace,
|
|
699
|
-
snapshot: snapshot,
|
|
700
|
-
runtimeDir: effectiveRuntimeDir,
|
|
701
|
-
onDiagnostic: onDiagnostic,
|
|
702
|
-
),
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
final runnerEnvironment = <String, String>{
|
|
706
|
-
'NOMOS_OFFLINE': '1',
|
|
707
|
-
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
708
|
-
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
709
|
-
if (info['devicePublic'] != null)
|
|
710
|
-
'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
|
|
711
|
-
if (info['deviceSecret'] != null)
|
|
712
|
-
'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
|
|
713
|
-
if (effectiveRuntimeDir != null)
|
|
714
|
-
'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
|
|
715
|
-
};
|
|
716
|
-
transport = await _phased(
|
|
717
|
-
phase: '2/4: spawn the node runner',
|
|
718
|
-
awaiting: 'Process.start($node)',
|
|
583
|
+
final peer = await _phased(
|
|
584
|
+
phase: '1/2: open canonical test peer',
|
|
585
|
+
awaiting: 'Peer Office + filesystem Git custody',
|
|
719
586
|
timeout: phaseTimeout ?? _kSpawnDeadline,
|
|
720
587
|
onDiagnostic: onDiagnostic,
|
|
721
|
-
run: () =>
|
|
588
|
+
run: () => NmsTestPeer.start(
|
|
722
589
|
entry: entry,
|
|
590
|
+
runtimeDirectory: effectiveRuntimeDir,
|
|
723
591
|
resolveFrom: resolveFrom,
|
|
724
592
|
node: node,
|
|
725
|
-
environment: runnerEnvironment,
|
|
726
593
|
onDiagnostic: onDiagnostic,
|
|
727
594
|
),
|
|
728
595
|
);
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
'the
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
596
|
+
transport = peer.transport;
|
|
597
|
+
workdir = peer.custody.directory;
|
|
598
|
+
var plane = peer.reception;
|
|
599
|
+
// An application home is warranted by certificates the founding device signs, so that key is
|
|
600
|
+
// minted FIRST and the peer is booted to trust it (typed boot configuration) before genesis.
|
|
601
|
+
protocol.SigningKeyPair? founder;
|
|
602
|
+
if (birthsApplicationHome) {
|
|
603
|
+
founder = await _phased(
|
|
604
|
+
phase: '1b/2: mint the founding device and boot the peer to trust it',
|
|
605
|
+
awaiting: 'authorKeygen + peer reboot with --trust-root-public-key',
|
|
606
|
+
timeout: phaseTimeout ?? _kSpawnDeadline,
|
|
607
|
+
onDiagnostic: onDiagnostic,
|
|
608
|
+
run: () async {
|
|
609
|
+
final key = await peer.mintFoundingDevice();
|
|
610
|
+
await peer.reboot(trustRootPublicKey: key.publicKey);
|
|
611
|
+
return key;
|
|
612
|
+
},
|
|
613
|
+
);
|
|
614
|
+
transport = peer.transport;
|
|
615
|
+
plane = peer.reception;
|
|
616
|
+
}
|
|
617
|
+
// THE OFFICE NAMES THE APPLICATION HOME. The harness holds no copy of the
|
|
618
|
+
// receiving-group derivation; it asks the peer it just started.
|
|
619
|
+
final openWorkspace = birthsApplicationHome
|
|
620
|
+
? await plane.nmsReceivingGroupWorkspace(owner)
|
|
621
|
+
: installWorkspace;
|
|
622
|
+
final installed = await _phased(
|
|
623
|
+
phase: '2/2: create Git custody and install law',
|
|
743
624
|
workspace: installWorkspace,
|
|
744
|
-
awaiting: '
|
|
625
|
+
awaiting: 'genesis offer + ordinary lifecycle offers',
|
|
745
626
|
timeout: phaseTimeout ?? connectTimeout,
|
|
746
627
|
onDiagnostic: onDiagnostic,
|
|
747
|
-
|
|
748
|
-
'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
|
|
749
|
-
'run with onDiagnostic to see where the runner blocked',
|
|
750
|
-
],
|
|
751
|
-
run: () => plane.connect(
|
|
752
|
-
cloud:
|
|
753
|
-
'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
628
|
+
run: () => peer.installWorkspace(
|
|
754
629
|
workspace: installWorkspace,
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
allowUnborn: true,
|
|
758
|
-
// THE HARNESS DEVICE (era-0): the install lane mints one device key per plane — sessions
|
|
759
|
-
// sign with it, and the client completes the self-serve birth ceremony (enrol + cert)
|
|
760
|
-
// with the SAME key, so offline `.births()` offers Just Work.
|
|
761
|
-
authorSecret: info['deviceSecret']?.toString(),
|
|
630
|
+
lawUsdzPaths: lawUsdzPaths,
|
|
631
|
+
deviceKey: founder,
|
|
762
632
|
),
|
|
763
633
|
);
|
|
634
|
+
final info = <String, Object?>{
|
|
635
|
+
'devicePublic': base64Encode(installed.deviceKey.publicKey),
|
|
636
|
+
'deviceSecret': base64Encode(installed.deviceKey.secret),
|
|
637
|
+
'frameworkHash': installed.controllerHash,
|
|
638
|
+
'lawHashes': installed.lawHashes,
|
|
639
|
+
};
|
|
764
640
|
if (birthsApplicationHome) {
|
|
765
641
|
final maxChildWorkspaces = applicationMaxChildWorkspacesPerUser;
|
|
766
642
|
if (maxChildWorkspaces == null || maxChildWorkspaces < 0) {
|
|
@@ -799,7 +675,7 @@ class NomosTestHarness {
|
|
|
799
675
|
// scenario carries the value from the same package declaration that compiled the
|
|
800
676
|
// receiving-group plan; the harness does not choose or default it. The ordinary
|
|
801
677
|
// seed offer crosses the kernel author gate before the first birth is attempted.
|
|
802
|
-
run: () => plane.session(installWorkspace).
|
|
678
|
+
run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
803
679
|
call: applicationFrameworkCalls!.seedBirthPolicy(
|
|
804
680
|
domain: '__nomos_application_home_v1',
|
|
805
681
|
workspace: installWorkspace,
|
|
@@ -808,6 +684,7 @@ class NomosTestHarness {
|
|
|
808
684
|
setAt: bornAt,
|
|
809
685
|
),
|
|
810
686
|
actor: owner,
|
|
687
|
+
authorSecret: deviceSecret,
|
|
811
688
|
),
|
|
812
689
|
);
|
|
813
690
|
final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
|
|
@@ -817,29 +694,30 @@ class NomosTestHarness {
|
|
|
817
694
|
awaiting: 'law-declared enrollSigner offer',
|
|
818
695
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
819
696
|
onDiagnostic: onDiagnostic,
|
|
820
|
-
run: () => plane.session(installWorkspace).
|
|
697
|
+
run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
821
698
|
call: applicationFrameworkCalls!.enrollSigner(
|
|
822
699
|
domain: '__nomos_application_home_v1',
|
|
823
700
|
workspace: installWorkspace,
|
|
824
701
|
keyHash: keyHash,
|
|
825
702
|
grantedBy: owner,
|
|
826
703
|
grantedAt: 1,
|
|
827
|
-
verifiedVia: 'nomos:
|
|
704
|
+
verifiedVia: 'nomos:harness-root',
|
|
828
705
|
),
|
|
829
706
|
actor: owner,
|
|
707
|
+
authorSecret: deviceSecret,
|
|
830
708
|
),
|
|
831
709
|
);
|
|
832
710
|
final delegationCert =
|
|
833
711
|
await plane.session(installWorkspace).signBirthCert(
|
|
834
712
|
BirthCertificate(
|
|
835
|
-
child:
|
|
713
|
+
child: openWorkspace,
|
|
836
714
|
founder: owner,
|
|
837
715
|
verified: true,
|
|
838
716
|
parentKey: base64Decode(devicePublic),
|
|
839
717
|
parentId: 'root',
|
|
840
718
|
epoch: Int64.ZERO,
|
|
841
719
|
),
|
|
842
|
-
deviceSecret,
|
|
720
|
+
base64Decode(deviceSecret),
|
|
843
721
|
);
|
|
844
722
|
await _phased(
|
|
845
723
|
phase: 'application: record the parent-signed device delegation',
|
|
@@ -847,16 +725,17 @@ class NomosTestHarness {
|
|
|
847
725
|
awaiting: 'law-declared enrollDelegation offer',
|
|
848
726
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
849
727
|
onDiagnostic: onDiagnostic,
|
|
850
|
-
run: () => plane.session(installWorkspace).
|
|
728
|
+
run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
851
729
|
call: applicationFrameworkCalls!.enrollDelegation(
|
|
852
730
|
domain: '__nomos_application_home_v1',
|
|
853
731
|
workspace: installWorkspace,
|
|
854
732
|
user: owner,
|
|
855
|
-
deviceKey:
|
|
733
|
+
deviceKey: 'key:$keyHash',
|
|
856
734
|
certificate: delegationCert,
|
|
857
735
|
enrolledAt: '1970-01-01T00:00:00.000Z',
|
|
858
736
|
),
|
|
859
737
|
actor: owner,
|
|
738
|
+
authorSecret: deviceSecret,
|
|
860
739
|
),
|
|
861
740
|
);
|
|
862
741
|
final parentCert = await plane.session(installWorkspace).signBirthCert(
|
|
@@ -868,7 +747,7 @@ class NomosTestHarness {
|
|
|
868
747
|
parentId: 'root',
|
|
869
748
|
epoch: Int64.ZERO,
|
|
870
749
|
),
|
|
871
|
-
deviceSecret,
|
|
750
|
+
base64Decode(deviceSecret),
|
|
872
751
|
);
|
|
873
752
|
await _phased(
|
|
874
753
|
phase: 'application: attest and warrant the application parent',
|
|
@@ -877,7 +756,7 @@ class NomosTestHarness {
|
|
|
877
756
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
878
757
|
onDiagnostic: onDiagnostic,
|
|
879
758
|
run: () async {
|
|
880
|
-
await plane.session(installWorkspace).
|
|
759
|
+
await plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
881
760
|
call: applicationFrameworkCalls!.seedBirthAttestation(
|
|
882
761
|
domain: '__nomos_application_home_v1',
|
|
883
762
|
workspace: installWorkspace,
|
|
@@ -890,8 +769,9 @@ class NomosTestHarness {
|
|
|
890
769
|
links: <BirthCertificate>[parentCert],
|
|
891
770
|
),
|
|
892
771
|
actor: owner,
|
|
772
|
+
authorSecret: deviceSecret,
|
|
893
773
|
);
|
|
894
|
-
await plane.session(installWorkspace).
|
|
774
|
+
await plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
895
775
|
call: applicationFrameworkCalls.seedGenesis(
|
|
896
776
|
domain: '__nomos_application_home_v1',
|
|
897
777
|
workspace: installWorkspace,
|
|
@@ -900,27 +780,28 @@ class NomosTestHarness {
|
|
|
900
780
|
bornAt: '1970-01-01T00:00:00.000Z',
|
|
901
781
|
),
|
|
902
782
|
actor: owner,
|
|
783
|
+
authorSecret: deviceSecret,
|
|
903
784
|
);
|
|
904
785
|
},
|
|
905
786
|
);
|
|
906
787
|
final homeCert = await plane.session(installWorkspace).signBirthCert(
|
|
907
788
|
BirthCertificate(
|
|
908
|
-
child:
|
|
789
|
+
child: openWorkspace,
|
|
909
790
|
founder: owner,
|
|
910
791
|
verified: true,
|
|
911
792
|
parentKey: base64Decode(devicePublic),
|
|
912
793
|
parentId: installWorkspace,
|
|
913
794
|
epoch: Int64.ZERO,
|
|
914
795
|
),
|
|
915
|
-
deviceSecret,
|
|
796
|
+
base64Decode(deviceSecret),
|
|
916
797
|
);
|
|
917
798
|
final outcome = await _phased(
|
|
918
799
|
phase: 'application: birth the signed-in application',
|
|
919
|
-
workspace:
|
|
800
|
+
workspace: openWorkspace,
|
|
920
801
|
awaiting: 'attested receiving-group birth',
|
|
921
802
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
922
803
|
onDiagnostic: onDiagnostic,
|
|
923
|
-
run: () => plane.session(installWorkspace).
|
|
804
|
+
run: () => plane.session(installWorkspace).offerGeneratedCallOutcome(
|
|
924
805
|
call: applicationFrameworkCalls!.birthReceivingGroup(
|
|
925
806
|
domain: '__nomos_application_home_v1',
|
|
926
807
|
workspace: installWorkspace,
|
|
@@ -936,32 +817,34 @@ class NomosTestHarness {
|
|
|
936
817
|
);
|
|
937
818
|
String? bornStatus;
|
|
938
819
|
for (final entry in outcome.birthOutcomes) {
|
|
939
|
-
if (entry.workspace ==
|
|
820
|
+
if (entry.workspace == openWorkspace) {
|
|
940
821
|
bornStatus = entry.status.name;
|
|
941
822
|
break;
|
|
942
823
|
}
|
|
943
824
|
}
|
|
944
825
|
if (outcome.born.length != 1 ||
|
|
945
|
-
outcome.born.single !=
|
|
826
|
+
outcome.born.single != openWorkspace ||
|
|
946
827
|
bornStatus != 'BIRTH_OUTCOME_STATUS_BORN') {
|
|
947
828
|
throw NomosTestHarnessException(
|
|
948
|
-
'the offline application parent did not birth "$
|
|
829
|
+
'the offline application parent did not birth "$openWorkspace" cleanly '
|
|
949
830
|
'(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})',
|
|
950
831
|
);
|
|
951
832
|
}
|
|
952
833
|
await _phased(
|
|
953
834
|
phase: 'application: mount the signed-in application',
|
|
954
|
-
workspace:
|
|
835
|
+
workspace: openWorkspace,
|
|
955
836
|
awaiting: 'connect born receiving group',
|
|
956
837
|
timeout: phaseTimeout ?? connectTimeout,
|
|
957
838
|
onDiagnostic: onDiagnostic,
|
|
958
|
-
run: ()
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
839
|
+
run: () async {
|
|
840
|
+
await plane.bindSession(
|
|
841
|
+
workspace: openWorkspace,
|
|
842
|
+
session: openWorkspace,
|
|
843
|
+
);
|
|
844
|
+
await plane.session(openWorkspace).refreshCredentials(
|
|
845
|
+
authorSecret: deviceSecret,
|
|
846
|
+
);
|
|
847
|
+
},
|
|
965
848
|
);
|
|
966
849
|
} else if (owner != null && owner.isNotEmpty) {
|
|
967
850
|
final devicePublic = info['devicePublic']?.toString();
|
|
@@ -978,22 +861,22 @@ class NomosTestHarness {
|
|
|
978
861
|
final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
|
|
979
862
|
await _phased(
|
|
980
863
|
phase: 'owner: seat the test device signer',
|
|
981
|
-
workspace:
|
|
864
|
+
workspace: openWorkspace,
|
|
982
865
|
awaiting: 'verified test-root signer enrollment',
|
|
983
866
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
984
867
|
onDiagnostic: onDiagnostic,
|
|
985
868
|
// A generated TestPerson stands in for a real signed-in person. Seat the throwaway
|
|
986
869
|
// harness device through the installed framework law with the same verified provenance
|
|
987
870
|
// that the private test root later attests. Production clients never manufacture this
|
|
988
|
-
// marker; it exists only inside the offline
|
|
989
|
-
run: () => plane.session(
|
|
871
|
+
// marker; it exists only inside the offline harness ceremony.
|
|
872
|
+
run: () => plane.session(openWorkspace).offerGeneratedCallOutcome(
|
|
990
873
|
call: applicationFrameworkCalls!.enrollSigner(
|
|
991
874
|
domain: '__nomos_application_home_v1',
|
|
992
|
-
workspace:
|
|
875
|
+
workspace: openWorkspace,
|
|
993
876
|
keyHash: keyHash,
|
|
994
877
|
grantedBy: owner,
|
|
995
878
|
grantedAt: 1,
|
|
996
|
-
verifiedVia: 'nomos:
|
|
879
|
+
verifiedVia: 'nomos:harness-root',
|
|
997
880
|
),
|
|
998
881
|
actor: owner,
|
|
999
882
|
),
|
|
@@ -1012,25 +895,25 @@ class NomosTestHarness {
|
|
|
1012
895
|
final bornAt = DateTime.now().toUtc().toIso8601String();
|
|
1013
896
|
await _phased(
|
|
1014
897
|
phase: 'owner: make the test application ready',
|
|
1015
|
-
workspace:
|
|
898
|
+
workspace: openWorkspace,
|
|
1016
899
|
awaiting: 'framework owner birth',
|
|
1017
900
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
1018
901
|
onDiagnostic: onDiagnostic,
|
|
1019
902
|
run: () async {
|
|
1020
|
-
await plane.session(
|
|
903
|
+
await plane.session(openWorkspace).offerGeneratedCallOutcome(
|
|
1021
904
|
call: applicationFrameworkCalls!.seedReceivingGroupPolicy(
|
|
1022
905
|
domain: '__nomos_application_home_v1',
|
|
1023
|
-
workspace:
|
|
906
|
+
workspace: openWorkspace,
|
|
1024
907
|
setAt: bornAt,
|
|
1025
908
|
),
|
|
1026
909
|
actor: 'host-birth',
|
|
1027
910
|
);
|
|
1028
|
-
await plane.session(
|
|
911
|
+
await plane.session(openWorkspace).offerGeneratedCallOutcome(
|
|
1029
912
|
call: applicationFrameworkCalls.seedReceivingGroup(
|
|
1030
913
|
domain: '__nomos_application_home_v1',
|
|
1031
|
-
workspace:
|
|
914
|
+
workspace: openWorkspace,
|
|
1032
915
|
principal: owner,
|
|
1033
|
-
workspaceName:
|
|
916
|
+
workspaceName: openWorkspace,
|
|
1034
917
|
bornAt: bornAt,
|
|
1035
918
|
),
|
|
1036
919
|
actor: 'host-birth',
|
|
@@ -1039,52 +922,48 @@ class NomosTestHarness {
|
|
|
1039
922
|
);
|
|
1040
923
|
final parentCert = await plane.signBirthCert(
|
|
1041
924
|
BirthCertificate(
|
|
1042
|
-
child:
|
|
925
|
+
child: openWorkspace,
|
|
1043
926
|
founder: owner,
|
|
1044
927
|
verified: true,
|
|
1045
928
|
parentKey: base64Decode(devicePublic),
|
|
1046
929
|
parentId: 'root',
|
|
1047
930
|
epoch: Int64.ZERO,
|
|
1048
931
|
),
|
|
1049
|
-
deviceSecret,
|
|
932
|
+
base64Decode(deviceSecret),
|
|
1050
933
|
);
|
|
1051
934
|
await _phased(
|
|
1052
935
|
phase: 'owner: establish the verified test application root',
|
|
1053
|
-
workspace:
|
|
936
|
+
workspace: openWorkspace,
|
|
1054
937
|
awaiting: 'birth attestation then genesis warrant',
|
|
1055
938
|
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
1056
939
|
onDiagnostic: onDiagnostic,
|
|
1057
940
|
run: () async {
|
|
1058
|
-
await plane.session(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
'workspaceId': workspace,
|
|
1085
|
-
'bornAt': '2000-01-01T00:00:00.000Z',
|
|
1086
|
-
},
|
|
1087
|
-
);
|
|
941
|
+
await plane.session(openWorkspace).offerGeneratedCallOutcome(
|
|
942
|
+
call: applicationFrameworkCalls!.seedBirthAttestation(
|
|
943
|
+
domain: '__nomos_application_home_v1',
|
|
944
|
+
workspace: openWorkspace,
|
|
945
|
+
child: openWorkspace,
|
|
946
|
+
founder: owner,
|
|
947
|
+
verified: 'true',
|
|
948
|
+
parentId: 'root',
|
|
949
|
+
parentKey: devicePublic,
|
|
950
|
+
epoch: 0,
|
|
951
|
+
links: <BirthCertificate>[parentCert],
|
|
952
|
+
),
|
|
953
|
+
actor: owner,
|
|
954
|
+
authorSecret: deviceSecret,
|
|
955
|
+
);
|
|
956
|
+
await plane.session(openWorkspace).offerGeneratedCallOutcome(
|
|
957
|
+
call: applicationFrameworkCalls.seedGenesis(
|
|
958
|
+
domain: '__nomos_application_home_v1',
|
|
959
|
+
workspace: openWorkspace,
|
|
960
|
+
principal: owner,
|
|
961
|
+
workspaceId: openWorkspace,
|
|
962
|
+
bornAt: '2000-01-01T00:00:00.000Z',
|
|
963
|
+
),
|
|
964
|
+
actor: owner,
|
|
965
|
+
authorSecret: deviceSecret,
|
|
966
|
+
);
|
|
1088
967
|
},
|
|
1089
968
|
);
|
|
1090
969
|
} on DomainNotInstalledForSession {
|
|
@@ -1105,12 +984,12 @@ class NomosTestHarness {
|
|
|
1105
984
|
? installedHashes.last
|
|
1106
985
|
: installedHashes.first)
|
|
1107
986
|
.substring(0, 12);
|
|
1108
|
-
phase('ready — session "$
|
|
987
|
+
phase('ready — session "$openWorkspace" connected (law $sessionLaw)');
|
|
1109
988
|
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
1110
989
|
// single-workspace assumptions ship — with one session connected, an op that forgot its session
|
|
1111
990
|
// silently works in tests and only refuses in production when the app connects its second
|
|
1112
991
|
// workspace. The decoy keeps the plane at ≥2 sessions so any unpinned op refuses HERE, in the
|
|
1113
|
-
// dev's first test. [
|
|
992
|
+
// dev's first test. [reception] is a SESSION HANDLE pinned to the opened workspace, so tenant code written
|
|
1114
993
|
// against the harness keeps working unchanged; only code that grabs the raw plane and assumes
|
|
1115
994
|
// single-session trips. Opt out with `decoySession: false` (please don't).
|
|
1116
995
|
if (decoySession && !birthsApplicationHome) {
|
|
@@ -1120,60 +999,42 @@ class NomosTestHarness {
|
|
|
1120
999
|
awaiting: 'connect',
|
|
1121
1000
|
timeout: phaseTimeout ?? connectTimeout,
|
|
1122
1001
|
onDiagnostic: onDiagnostic,
|
|
1123
|
-
run: () async => plane.
|
|
1124
|
-
cloud: 'https://offline.invalid',
|
|
1002
|
+
run: () async => plane.bindSession(
|
|
1125
1003
|
workspace: _decoyWorkspace,
|
|
1126
1004
|
session: _decoyWorkspace,
|
|
1127
|
-
clientId: '$clientId-decoy',
|
|
1128
|
-
allowUnborn: true,
|
|
1129
|
-
restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
|
|
1130
1005
|
),
|
|
1131
1006
|
);
|
|
1132
1007
|
}
|
|
1133
1008
|
final harness = NomosTestHarness._(
|
|
1134
|
-
plane.session(
|
|
1135
|
-
|
|
1009
|
+
plane.session(openWorkspace),
|
|
1010
|
+
openWorkspace,
|
|
1136
1011
|
transport,
|
|
1137
1012
|
workdir,
|
|
1138
1013
|
onDiagnostic,
|
|
1139
|
-
[
|
|
1014
|
+
[openWorkspace],
|
|
1140
1015
|
devicePublics: {
|
|
1141
1016
|
if (info['devicePublic'] != null)
|
|
1142
|
-
|
|
1017
|
+
openWorkspace: info['devicePublic'].toString(),
|
|
1143
1018
|
},
|
|
1144
1019
|
deviceSecrets: {
|
|
1145
1020
|
if (info['deviceSecret'] != null)
|
|
1146
|
-
|
|
1021
|
+
openWorkspace: info['deviceSecret'].toString(),
|
|
1147
1022
|
},
|
|
1148
1023
|
frameworkHash: info['frameworkHash']?.toString(),
|
|
1149
1024
|
lawHashes: {
|
|
1150
|
-
|
|
1025
|
+
openWorkspace: ((info['lawHashes'] as List?) ?? const [])
|
|
1151
1026
|
.map((e) => e.toString())
|
|
1152
1027
|
.toList(),
|
|
1153
1028
|
},
|
|
1154
|
-
|
|
1155
|
-
|
|
1029
|
+
domainKeys: {
|
|
1030
|
+
openWorkspace: installed.domainKeys,
|
|
1031
|
+
},
|
|
1032
|
+
testPeer: peer,
|
|
1033
|
+
setupSessions: {
|
|
1034
|
+
if (birthsApplicationHome) installWorkspace,
|
|
1156
1035
|
},
|
|
1157
|
-
installLane: _InstallLane(
|
|
1158
|
-
node: node,
|
|
1159
|
-
useInRepoScript: useInRepoScript,
|
|
1160
|
-
inRepoScript: resolvedHelper,
|
|
1161
|
-
githolonCmd: githolonCmd,
|
|
1162
|
-
runtimeDir: effectiveRuntimeDir,
|
|
1163
|
-
clientId: clientId,
|
|
1164
|
-
),
|
|
1165
1036
|
phaseTimeout: phaseTimeout,
|
|
1166
|
-
|
|
1167
|
-
entry: entry,
|
|
1168
|
-
node: node,
|
|
1169
|
-
cloud: 'https://offline.invalid',
|
|
1170
|
-
authToken: '',
|
|
1171
|
-
authorSecret: info['deviceSecret']?.toString() ?? '',
|
|
1172
|
-
clientId: clientId,
|
|
1173
|
-
resolveFrom: resolveFrom,
|
|
1174
|
-
environment: runnerEnvironment,
|
|
1175
|
-
),
|
|
1176
|
-
homeWorkspace: workspace,
|
|
1037
|
+
homeWorkspace: openWorkspace,
|
|
1177
1038
|
);
|
|
1178
1039
|
if (autoTearDown && registerTearDown != null) {
|
|
1179
1040
|
registerTearDown(harness.dispose);
|
|
@@ -1187,17 +1048,16 @@ class NomosTestHarness {
|
|
|
1187
1048
|
} catch (_) {}
|
|
1188
1049
|
}
|
|
1189
1050
|
try {
|
|
1190
|
-
await workdir
|
|
1051
|
+
await workdir?.delete(recursive: true);
|
|
1191
1052
|
} catch (_) {}
|
|
1192
1053
|
rethrow;
|
|
1193
1054
|
}
|
|
1194
1055
|
}
|
|
1195
1056
|
|
|
1196
1057
|
/// A COLOCATED TEST CLUSTER — install MANY workspaces' law offline and connect one SESSION per workspace on
|
|
1197
|
-
/// ONE
|
|
1198
|
-
/// reads, foreign-source imports) runs in-process with ZERO network. Each workspace
|
|
1199
|
-
///
|
|
1200
|
-
/// the per-connect restore snapshot loads it into the shared plane.
|
|
1058
|
+
/// ONE kernel host (one runner, one transport, one [reception]), so a cross-workspace scenario (attested
|
|
1059
|
+
/// reads, foreign-source imports) runs in-process with ZERO network. Each workspace has separate real Git
|
|
1060
|
+
/// custody, while every operation enters the same Peer Office and kernel.
|
|
1201
1061
|
///
|
|
1202
1062
|
/// [workspaces] — `{ workspaceName: [lawUsdzPaths] }` (installed in order per workspace). Get a
|
|
1203
1063
|
/// session-scoped view with [session] and bind it to a generated client: `EstateClient(harness.session(
|
|
@@ -1212,7 +1072,6 @@ class NomosTestHarness {
|
|
|
1212
1072
|
String clientId = 'test-runner',
|
|
1213
1073
|
String? runtimeDir,
|
|
1214
1074
|
String? nodeEntry,
|
|
1215
|
-
String? installHelper,
|
|
1216
1075
|
String? resolveFrom,
|
|
1217
1076
|
String node = 'node',
|
|
1218
1077
|
bool autoTearDown = true,
|
|
@@ -1234,218 +1093,75 @@ class NomosTestHarness {
|
|
|
1234
1093
|
);
|
|
1235
1094
|
}
|
|
1236
1095
|
}
|
|
1096
|
+
for (final workspace in deviceKeyed) {
|
|
1097
|
+
if (!workspaces.containsKey(workspace)) {
|
|
1098
|
+
throw NomosTestHarnessException(
|
|
1099
|
+
'deviceKeyed names "$workspace" but it is not a workspace of this cluster — '
|
|
1100
|
+
'workspaces: ${workspaces.keys.toList()}',
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1237
1104
|
final entry = nodeEntry ??
|
|
1238
|
-
await
|
|
1105
|
+
await _resolveTestEntry(node, resolveFrom ?? Directory.current.path);
|
|
1239
1106
|
final effectiveRuntimeDir = runtimeDir ??
|
|
1240
1107
|
Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
|
|
1241
1108
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
1242
1109
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
1243
|
-
await _sourceCandidateRuntime(entry)
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
(File(adjacentInstaller).existsSync()
|
|
1247
|
-
? adjacentInstaller
|
|
1248
|
-
: '${File(entry).parent.path}/offline-install.ts');
|
|
1249
|
-
final useInRepoScript =
|
|
1250
|
-
(resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
|
|
1251
|
-
File(resolvedHelper).existsSync();
|
|
1252
|
-
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
1253
|
-
|
|
1254
|
-
final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
|
|
1255
|
-
NodeRunnerTransport? transport;
|
|
1110
|
+
await _sourceCandidateRuntime(entry) ??
|
|
1111
|
+
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
1112
|
+
NmsTestPeer? peer;
|
|
1256
1113
|
try {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
final installDevicePublics = <String, String>{};
|
|
1261
|
-
final installDeviceSecrets = <String, String>{};
|
|
1262
|
-
String? planeTestRootPublic;
|
|
1263
|
-
String? planeTestRootSecret;
|
|
1264
|
-
String? frameworkHash;
|
|
1265
|
-
var _i = 0;
|
|
1266
|
-
for (final e in workspaces.entries) {
|
|
1267
|
-
final snap = '${workdir.path}/${e.key}.bin';
|
|
1268
|
-
final info = await _phased(
|
|
1269
|
-
phase: '1: offline law install ${++_i}/${workspaces.length}',
|
|
1270
|
-
workspace: e.key,
|
|
1271
|
-
awaiting: useInRepoScript
|
|
1272
|
-
? 'offline-install.ts'
|
|
1273
|
-
: 'githolon harness-install',
|
|
1274
|
-
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
1275
|
-
onDiagnostic: onDiagnostic,
|
|
1276
|
-
likelyCauses: const [
|
|
1277
|
-
'a cold npx download of githolon (pre-warm with any `githolon` run)',
|
|
1278
|
-
'a very large law package folding through the offline gate — raise phaseTimeout',
|
|
1279
|
-
],
|
|
1280
|
-
run: () => _runInstall(
|
|
1281
|
-
node: node,
|
|
1282
|
-
useInRepoScript: useInRepoScript,
|
|
1283
|
-
inRepoScript: resolvedHelper,
|
|
1284
|
-
githolonCmd: githolonCmd,
|
|
1285
|
-
lawUsdzPaths: e.value,
|
|
1286
|
-
workspace: e.key,
|
|
1287
|
-
snapshot: snap,
|
|
1288
|
-
runtimeDir: effectiveRuntimeDir,
|
|
1289
|
-
onDiagnostic: onDiagnostic,
|
|
1290
|
-
),
|
|
1291
|
-
);
|
|
1292
|
-
snapshots[e.key] = snap;
|
|
1293
|
-
lawHashesByWs[e.key] = ((info['lawHashes'] as List?) ?? const [])
|
|
1294
|
-
.map((x) => x.toString())
|
|
1295
|
-
.toList();
|
|
1296
|
-
frameworkHash ??= info['frameworkHash']?.toString();
|
|
1297
|
-
// THE HARNESS DEVICE (era-0): the install lane mints one device key per workspace — sessions sign
|
|
1298
|
-
// with it and the client completes the self-serve birth ceremony (enrol + cert) with the SAME key.
|
|
1299
|
-
if (info['devicePublic'] != null)
|
|
1300
|
-
installDevicePublics[e.key] = info['devicePublic'].toString();
|
|
1301
|
-
if (info['deviceSecret'] != null)
|
|
1302
|
-
installDeviceSecrets[e.key] = info['deviceSecret'].toString();
|
|
1303
|
-
planeTestRootPublic ??= info['devicePublic']?.toString();
|
|
1304
|
-
planeTestRootSecret ??= info['deviceSecret']?.toString();
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
|
|
1308
|
-
final phase = (String m) {
|
|
1309
|
-
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
1310
|
-
};
|
|
1311
|
-
transport = await _phased(
|
|
1312
|
-
phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
|
|
1313
|
-
awaiting: 'Process.start($node)',
|
|
1114
|
+
peer = await _phased(
|
|
1115
|
+
phase: '1/2: open canonical test peer',
|
|
1116
|
+
awaiting: 'Peer Office + filesystem Git custody',
|
|
1314
1117
|
timeout: phaseTimeout ?? _kSpawnDeadline,
|
|
1315
1118
|
onDiagnostic: onDiagnostic,
|
|
1316
|
-
run: () =>
|
|
1119
|
+
run: () => NmsTestPeer.start(
|
|
1317
1120
|
entry: entry,
|
|
1121
|
+
runtimeDirectory: effectiveRuntimeDir,
|
|
1318
1122
|
resolveFrom: resolveFrom,
|
|
1319
1123
|
node: node,
|
|
1320
|
-
environment: {
|
|
1321
|
-
'NOMOS_OFFLINE': '1',
|
|
1322
|
-
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
1323
|
-
if (planeTestRootPublic != null)
|
|
1324
|
-
'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
|
|
1325
|
-
if (planeTestRootSecret != null)
|
|
1326
|
-
'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
|
|
1327
|
-
if (effectiveRuntimeDir != null)
|
|
1328
|
-
'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
|
|
1329
|
-
},
|
|
1330
1124
|
onDiagnostic: onDiagnostic,
|
|
1331
1125
|
),
|
|
1332
1126
|
);
|
|
1333
|
-
final
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
likelyCauses: const [
|
|
1340
|
-
'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + framework law archives)',
|
|
1341
|
-
'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
|
|
1342
|
-
],
|
|
1343
|
-
run: () => bridge.ready,
|
|
1344
|
-
);
|
|
1345
|
-
|
|
1346
|
-
// 3. connect one SESSION per workspace, each restoring its OWN snapshot — LOCAL-ONLY (no cloud;
|
|
1347
|
-
// these are local githolons on the device plane, offline by nature).
|
|
1348
|
-
final snapshotB64 = <String, String>{};
|
|
1349
|
-
var _n = 0;
|
|
1127
|
+
final lawHashesByWs = <String, List<String>>{};
|
|
1128
|
+
final domainKeysByWs = <String, Set<String>>{};
|
|
1129
|
+
final devicePublics = <String, String>{};
|
|
1130
|
+
final deviceSecrets = <String, String>{};
|
|
1131
|
+
String? frameworkHash;
|
|
1132
|
+
var index = 0;
|
|
1350
1133
|
for (final e in workspaces.entries) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
);
|
|
1354
|
-
await _phased(
|
|
1355
|
-
phase: '3: connect session ${++_n}/${workspaces.length}',
|
|
1134
|
+
final installed = await _phased(
|
|
1135
|
+
phase: '2/2: install workspace ${++index}/${workspaces.length}',
|
|
1356
1136
|
workspace: e.key,
|
|
1357
|
-
awaiting: '
|
|
1358
|
-
timeout: phaseTimeout ??
|
|
1137
|
+
awaiting: 'genesis offer + ordinary lifecycle offers',
|
|
1138
|
+
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
1359
1139
|
onDiagnostic: onDiagnostic,
|
|
1360
|
-
|
|
1361
|
-
'a large ledger converge-fold can be slow — raise connectTimeout/phaseTimeout',
|
|
1362
|
-
'run with onDiagnostic to see where the runner blocked',
|
|
1363
|
-
],
|
|
1364
|
-
run: () => bridge.connect(
|
|
1140
|
+
run: () => peer!.installWorkspace(
|
|
1365
1141
|
workspace: e.key,
|
|
1366
|
-
|
|
1367
|
-
clientId: '$clientId-${e.key}',
|
|
1368
|
-
restoreFrom: snapshotB64[e.key],
|
|
1369
|
-
allowUnborn: true,
|
|
1370
|
-
// signed with the install-lane harness device (era-0 self-serve birth ceremony key)
|
|
1371
|
-
authorSecret: installDeviceSecrets[e.key],
|
|
1372
|
-
),
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
phase('cluster: ${workspaces.length} session(s) connected');
|
|
1376
|
-
|
|
1377
|
-
// 3b. DEVICE-KEY the named sessions: mint a fresh in-kernel key on each (now that it is mounted) and
|
|
1378
|
-
// RE-OPEN it with that key as its signing authorSecret — the resolver's attested-read signing key.
|
|
1379
|
-
// The re-open is the runner's ATOMIC session replace (dispose-then-connect under the hood); each
|
|
1380
|
-
// leg runs as a named, deadlined phase so a wedge is a typed HarnessPhaseTimeout, never a hang.
|
|
1381
|
-
final devicePublics = <String, String>{};
|
|
1382
|
-
final deviceSecretsKeyed = <String, String>{};
|
|
1383
|
-
for (final ws in deviceKeyed) {
|
|
1384
|
-
if (!workspaces.containsKey(ws)) {
|
|
1385
|
-
throw NomosTestHarnessException(
|
|
1386
|
-
'deviceKeyed names "$ws" but it is not a workspace of this cluster — '
|
|
1387
|
-
'workspaces: ${workspaces.keys.toList()}',
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
final kp = await _phased(
|
|
1391
|
-
phase: '3b: device keygen',
|
|
1392
|
-
workspace: ws,
|
|
1393
|
-
awaiting: 'authorKeygen',
|
|
1394
|
-
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
1395
|
-
onDiagnostic: onDiagnostic,
|
|
1396
|
-
run: () => bridge.session(ws).authorKeygen(),
|
|
1397
|
-
);
|
|
1398
|
-
devicePublics[ws] = (kp['public'] ?? '').toString();
|
|
1399
|
-
deviceSecretsKeyed[ws] = (kp['secret'] ?? '').toString();
|
|
1400
|
-
await _phased(
|
|
1401
|
-
phase: "3b: keyed reconnect of '$ws'",
|
|
1402
|
-
workspace: ws,
|
|
1403
|
-
awaiting: 'connect',
|
|
1404
|
-
timeout: phaseTimeout ?? connectTimeout,
|
|
1405
|
-
onDiagnostic: onDiagnostic,
|
|
1406
|
-
likelyCauses: const [
|
|
1407
|
-
'the runner never answered the re-connect of an already-open session — '
|
|
1408
|
-
'update @githolon/client (the session re-open is an atomic replace)',
|
|
1409
|
-
'a large ledger re-fold on the re-mount — raise connectTimeout/phaseTimeout',
|
|
1410
|
-
],
|
|
1411
|
-
run: () => bridge.connect(
|
|
1412
|
-
workspace: ws,
|
|
1413
|
-
session: ws,
|
|
1414
|
-
clientId: '$clientId-$ws',
|
|
1415
|
-
restoreFrom: snapshotB64[ws],
|
|
1416
|
-
authorSecret: (kp['secret'] ?? '').toString(),
|
|
1417
|
-
allowUnborn: true,
|
|
1142
|
+
lawUsdzPaths: e.value,
|
|
1418
1143
|
),
|
|
1419
1144
|
);
|
|
1145
|
+
lawHashesByWs[e.key] = installed.lawHashes;
|
|
1146
|
+
domainKeysByWs[e.key] = installed.domainKeys;
|
|
1147
|
+
frameworkHash ??= installed.controllerHash;
|
|
1148
|
+
devicePublics[e.key] = base64Encode(installed.deviceKey.publicKey);
|
|
1149
|
+
deviceSecrets[e.key] = base64Encode(installed.deviceKey.secret);
|
|
1420
1150
|
}
|
|
1421
|
-
if (deviceKeyed.isNotEmpty) {
|
|
1422
|
-
phase(
|
|
1423
|
-
'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened',
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
1151
|
final names = workspaces.keys.toList();
|
|
1428
1152
|
final harness = NomosTestHarness._(
|
|
1429
|
-
|
|
1153
|
+
peer!.reception,
|
|
1430
1154
|
names.first,
|
|
1431
|
-
transport,
|
|
1432
|
-
|
|
1155
|
+
peer.transport,
|
|
1156
|
+
peer.custody.directory,
|
|
1433
1157
|
onDiagnostic,
|
|
1434
1158
|
names,
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
deviceSecrets: {...installDeviceSecrets, ...deviceSecretsKeyed},
|
|
1159
|
+
devicePublics: devicePublics,
|
|
1160
|
+
deviceSecrets: deviceSecrets,
|
|
1438
1161
|
frameworkHash: frameworkHash,
|
|
1439
1162
|
lawHashes: lawHashesByWs,
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
node: node,
|
|
1443
|
-
useInRepoScript: useInRepoScript,
|
|
1444
|
-
inRepoScript: resolvedHelper,
|
|
1445
|
-
githolonCmd: githolonCmd,
|
|
1446
|
-
runtimeDir: effectiveRuntimeDir,
|
|
1447
|
-
clientId: clientId,
|
|
1448
|
-
),
|
|
1163
|
+
domainKeys: domainKeysByWs,
|
|
1164
|
+
testPeer: peer,
|
|
1449
1165
|
phaseTimeout: phaseTimeout,
|
|
1450
1166
|
);
|
|
1451
1167
|
if (autoTearDown && registerTearDown != null) {
|
|
@@ -1453,23 +1169,16 @@ class NomosTestHarness {
|
|
|
1453
1169
|
}
|
|
1454
1170
|
return harness;
|
|
1455
1171
|
} catch (_) {
|
|
1456
|
-
|
|
1457
|
-
try {
|
|
1458
|
-
await transport.dispose();
|
|
1459
|
-
} catch (_) {}
|
|
1460
|
-
}
|
|
1461
|
-
try {
|
|
1462
|
-
await workdir.delete(recursive: true);
|
|
1463
|
-
} catch (_) {}
|
|
1172
|
+
await peer?.dispose();
|
|
1464
1173
|
rethrow;
|
|
1465
1174
|
}
|
|
1466
1175
|
}
|
|
1467
1176
|
|
|
1468
1177
|
/// CLOUD-BOUND HARNESS (co2's real-app topology): a [NomosTestCloud] plays the remote control plane, the
|
|
1469
1178
|
/// REAL authenticated home-birth ceremony runs REMOTELY on [platformWorkspace], and the born home is
|
|
1470
|
-
/// mounted into ONE local
|
|
1179
|
+
/// mounted into ONE local kernel host as a normal cloud-bound [NomosSession] ([homeSession]). From there
|
|
1471
1180
|
/// the app shape holds exactly: generated `.births()` directives run LOCALLY (the app never clones the
|
|
1472
|
-
/// platform), children open as sibling sessions (`outcome.bornRefs.single.acquire(harness.
|
|
1181
|
+
/// platform), children open as sibling sessions (`outcome.bornRefs.single.acquire(harness.reception)`),
|
|
1473
1182
|
/// writes/projections work offline, and [converge] establishes remote custody parent-first when
|
|
1474
1183
|
/// connectivity returns ([NomosTestCloud.setOnline]). Entirely inside the test cloud's trust universe.
|
|
1475
1184
|
///
|
|
@@ -1503,6 +1212,11 @@ class NomosTestHarness {
|
|
|
1503
1212
|
Duration connectTimeout = const Duration(seconds: 120),
|
|
1504
1213
|
Duration? phaseTimeout,
|
|
1505
1214
|
bool decoySession = true,
|
|
1215
|
+
|
|
1216
|
+
/// Where the spawned node peer keeps its Git custody. A directory here makes that custody durable
|
|
1217
|
+
/// across the peer's restarts (the local-first restart lane); absent, the peer uses a temporary
|
|
1218
|
+
/// directory it removes on exit. NOMOS_NODE_PEER_CUSTODY names one for a whole test run.
|
|
1219
|
+
String? nodePeerCustody,
|
|
1506
1220
|
}) async {
|
|
1507
1221
|
if (lawUsdzPaths.isEmpty) {
|
|
1508
1222
|
throw NomosTestHarnessException(
|
|
@@ -1520,6 +1234,26 @@ class NomosTestHarness {
|
|
|
1520
1234
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
1521
1235
|
final bareUid =
|
|
1522
1236
|
subject.startsWith('user:') ? subject.substring(5) : subject;
|
|
1237
|
+
// THE OPERATOR SEAM, AND THE ONLY RAW HOST LANE LEFT IN THIS FILE.
|
|
1238
|
+
//
|
|
1239
|
+
// `cloudBound` stands a REMOTE test cloud up before any custody exists on
|
|
1240
|
+
// this device. In that moment the harness is acting as an OPERATOR, not as a
|
|
1241
|
+
// client: there is no session, no mounted workspace, and therefore no typed
|
|
1242
|
+
// office paper that could carry these — every Runtime v2 read answers from
|
|
1243
|
+
// local custody the harness does not yet have. Exactly three calls remain,
|
|
1244
|
+
// and each is a host control-plane route with no client equivalent:
|
|
1245
|
+
//
|
|
1246
|
+
// POST /v2/workspaces/<platform> boot the born platform (idempotent)
|
|
1247
|
+
// GET /v2/workspaces/<platform>/domains its installed law, before custody
|
|
1248
|
+
// POST /dev/mint-token the LOCAL dev server's test identity
|
|
1249
|
+
// provider — a fixture route, not a
|
|
1250
|
+
// Nomos host route at all
|
|
1251
|
+
//
|
|
1252
|
+
// What was here and is gone: a `GET /openapi.json` scrape whose only product
|
|
1253
|
+
// was a domain name the compiled framework calls already carry. Everything
|
|
1254
|
+
// after this phase — every offer, every read, the whole birth ceremony —
|
|
1255
|
+
// goes through the office. Nothing below may grow a fourth probe: if a
|
|
1256
|
+
// client would need it, it belongs on a desk.
|
|
1523
1257
|
final http = HttpClient();
|
|
1524
1258
|
Future<Map<String, Object?>> post(
|
|
1525
1259
|
String path,
|
|
@@ -1569,13 +1303,12 @@ class NomosTestHarness {
|
|
|
1569
1303
|
// server's in-process operator lane). Booting a born workspace is idempotent; an unborn one
|
|
1570
1304
|
// refuses 409, surfaced here with the fix.
|
|
1571
1305
|
String? frameworkHash;
|
|
1572
|
-
final frameworkPostRoutes =
|
|
1573
|
-
<({String domain, String directive, String path})>[];
|
|
1574
1306
|
final lawHashes = <String>[];
|
|
1307
|
+
late NomosBoundInteraction applicationInteraction;
|
|
1575
1308
|
await _phased(
|
|
1576
1309
|
phase: '1/6: boot the born platform + inspect installed law',
|
|
1577
1310
|
workspace: platformWorkspace,
|
|
1578
|
-
awaiting: '
|
|
1311
|
+
awaiting: 'boot the born platform + read its installed law',
|
|
1579
1312
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
1580
1313
|
onDiagnostic: onDiagnostic,
|
|
1581
1314
|
run: () async {
|
|
@@ -1623,30 +1356,6 @@ class NomosTestHarness {
|
|
|
1623
1356
|
in (((existing['currentLaw'] as Map?) ?? const {}).values)) {
|
|
1624
1357
|
rememberLaw(hash);
|
|
1625
1358
|
}
|
|
1626
|
-
final openapi = await get(
|
|
1627
|
-
'/v2/workspaces/$platformWorkspace/openapi.json',
|
|
1628
|
-
);
|
|
1629
|
-
final pathSets = <Map>[
|
|
1630
|
-
(openapi['paths'] as Map?) ?? const {},
|
|
1631
|
-
(((openapi['x-nomos-framework-routes'] as Map?)?['paths'])
|
|
1632
|
-
as Map?) ??
|
|
1633
|
-
const {},
|
|
1634
|
-
];
|
|
1635
|
-
for (final paths in pathSets) {
|
|
1636
|
-
for (final entry in paths.entries) {
|
|
1637
|
-
final operation = (entry.value as Map?)?['post'] as Map?;
|
|
1638
|
-
final route = operation?['x-nomos'] as Map?;
|
|
1639
|
-
final domain = route?['domain']?.toString() ?? '';
|
|
1640
|
-
final directive = route?['directiveId']?.toString() ?? '';
|
|
1641
|
-
if (domain.isNotEmpty && directive.isNotEmpty) {
|
|
1642
|
-
frameworkPostRoutes.add((
|
|
1643
|
-
domain: domain,
|
|
1644
|
-
directive: directive,
|
|
1645
|
-
path: entry.key.toString(),
|
|
1646
|
-
));
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
1359
|
},
|
|
1651
1360
|
);
|
|
1652
1361
|
if (frameworkHash == null || frameworkHash!.isEmpty) {
|
|
@@ -1659,19 +1368,13 @@ class NomosTestHarness {
|
|
|
1659
1368
|
'cloudBound: the born test platform reported no installed application law — cannot run the birth ceremony',
|
|
1660
1369
|
);
|
|
1661
1370
|
}
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
if (birthRoutes.length != 1) {
|
|
1670
|
-
throw NomosTestHarnessException(
|
|
1671
|
-
'cloudBound: the deployed package must expose exactly one framework receiving-group route; found ${birthRoutes.length}',
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
final applicationHomeDomain = birthRoutes.single.domain;
|
|
1371
|
+
// THE COMPILED DESCRIPTOR NAMES ITS OWN DOMAIN. This used to scrape the
|
|
1372
|
+
// platform's `/openapi.json` for the framework birth route and read the
|
|
1373
|
+
// domain off it — a raw JSON probe of the host to learn a fact the
|
|
1374
|
+
// application's generated framework calls already carry, and whose scraped
|
|
1375
|
+
// paths were never sent anything: remote offers go through
|
|
1376
|
+
// `nomosInternalOfferRemoteGeneratedCall`, which routes by generated call.
|
|
1377
|
+
final applicationHomeDomain = frameworkCalls.applicationHomeDomain;
|
|
1675
1378
|
// A composed application has two distinct laws at this seam:
|
|
1676
1379
|
// * the parent's CURRENT control law authors enrollFirstDevice + the birth offer;
|
|
1677
1380
|
// * the application's final PAYLOAD package is the law installed into the born receiving group.
|
|
@@ -1697,25 +1400,7 @@ class NomosTestHarness {
|
|
|
1697
1400
|
);
|
|
1698
1401
|
}
|
|
1699
1402
|
final applicationHomeLawHash = lawHashes.last;
|
|
1700
|
-
|
|
1701
|
-
.where(
|
|
1702
|
-
(route) =>
|
|
1703
|
-
route.directive == 'enrollFirstDevice' &&
|
|
1704
|
-
route.domain == applicationHomeDomain,
|
|
1705
|
-
)
|
|
1706
|
-
.toSet()
|
|
1707
|
-
.toList();
|
|
1708
|
-
if (enrollmentRoutes.length != 1) {
|
|
1709
|
-
throw NomosTestHarnessException(
|
|
1710
|
-
"cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}",
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
if (enrollmentRoutes.single.path.isEmpty) {
|
|
1714
|
-
throw NomosTestHarnessException(
|
|
1715
|
-
'cloudBound: enrollFirstDevice generated an empty relay path',
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
// 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
|
|
1403
|
+
// 2/6 — the LOCAL kernel host, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
|
|
1719
1404
|
// what mints the device key + signs the birth cert (era-0: a birth is ATTESTED or not at all).
|
|
1720
1405
|
// The executable AND its framework-law packages are one runtime bundle in device custody. A process
|
|
1721
1406
|
// restart while offline must not fetch any member before it can replay the persisted ledger. Release CI
|
|
@@ -1724,18 +1409,15 @@ class NomosTestHarness {
|
|
|
1724
1409
|
final candidateRuntimeDir = Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
1725
1410
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'];
|
|
1726
1411
|
final localRuntime = await Directory(
|
|
1727
|
-
'${workdir.path}/
|
|
1412
|
+
'${workdir.path}/peer-office-runtime',
|
|
1728
1413
|
).create(recursive: true);
|
|
1729
|
-
final selectedKernel = cloud.testKRootPublic == null
|
|
1730
|
-
? _CandidateKernelBinary.production
|
|
1731
|
-
: _CandidateKernelBinary.testMint;
|
|
1732
1414
|
final candidateArtifacts = candidateRuntimeDir == null
|
|
1733
1415
|
? const <_KernelRuntimeArtifact, String>{}
|
|
1734
1416
|
: <_KernelRuntimeArtifact, String>{
|
|
1735
1417
|
_KernelRuntimeArtifact.executable:
|
|
1736
|
-
'$candidateRuntimeDir
|
|
1418
|
+
'$candidateRuntimeDir/holon.wasm',
|
|
1737
1419
|
_KernelRuntimeArtifact.repository:
|
|
1738
|
-
'$candidateRuntimeDir
|
|
1420
|
+
'$candidateRuntimeDir/kernel.repository.xml',
|
|
1739
1421
|
_KernelRuntimeArtifact.stageZeroLaw:
|
|
1740
1422
|
'$candidateRuntimeDir/${_KernelRuntimeArtifact.stageZeroLaw.fileName}',
|
|
1741
1423
|
_KernelRuntimeArtifact.controllerLaw:
|
|
@@ -1781,12 +1463,22 @@ class NomosTestHarness {
|
|
|
1781
1463
|
},
|
|
1782
1464
|
);
|
|
1783
1465
|
}
|
|
1784
|
-
final
|
|
1785
|
-
|
|
1466
|
+
final custodyDirectory = nodePeerCustody ??
|
|
1467
|
+
Platform.environment['NOMOS_NODE_PEER_CUSTODY'] ??
|
|
1468
|
+
'${workdir.path}/device';
|
|
1469
|
+
final runnerArguments = <String>[
|
|
1470
|
+
'--runtime-bundle=${localRuntime.path}',
|
|
1471
|
+
'--discovery-provider=${cloud.origin}',
|
|
1786
1472
|
if (cloud.testKRootPublic != null)
|
|
1787
|
-
'
|
|
1788
|
-
|
|
1789
|
-
|
|
1473
|
+
'--trust-root-public-key=${cloud.testKRootPublic}',
|
|
1474
|
+
// Under --diagnostics the spawned peer narrates every frame (command → outcome) to its stderr,
|
|
1475
|
+
// which this harness relays as `Shell:` lines.
|
|
1476
|
+
if (Platform.environment['NOMOS_TEST_DIAGNOSTICS'] == '1')
|
|
1477
|
+
'--trace=frames',
|
|
1478
|
+
// Process memory is disposable; restart must reuse this device's disk.
|
|
1479
|
+
// Keep default custody under the harness-owned root until dispose.
|
|
1480
|
+
'--custody=$custodyDirectory',
|
|
1481
|
+
];
|
|
1790
1482
|
transport = await _phased(
|
|
1791
1483
|
phase: '2/6: spawn the node runner (cloud-bound)',
|
|
1792
1484
|
awaiting: 'Process.start($node)',
|
|
@@ -1796,11 +1488,11 @@ class NomosTestHarness {
|
|
|
1796
1488
|
entry: entry,
|
|
1797
1489
|
resolveFrom: resolveFrom,
|
|
1798
1490
|
node: node,
|
|
1799
|
-
|
|
1491
|
+
arguments: runnerArguments,
|
|
1800
1492
|
onDiagnostic: onDiagnostic,
|
|
1801
1493
|
),
|
|
1802
1494
|
);
|
|
1803
|
-
final plane =
|
|
1495
|
+
final plane = DartReception(transport!);
|
|
1804
1496
|
await _phased(
|
|
1805
1497
|
phase: '3/6: runner ready',
|
|
1806
1498
|
awaiting: "event:'ready'",
|
|
@@ -1826,21 +1518,18 @@ class NomosTestHarness {
|
|
|
1826
1518
|
// tolerated): a PRIVATE platform (declared read caps — co2's shape) refuses an
|
|
1827
1519
|
// unauthenticated ledger clone, and the ceremony never needs the platform's ledger anyway
|
|
1828
1520
|
// (its writes go over the HTTP gateway below).
|
|
1829
|
-
await plane.
|
|
1830
|
-
cloud: cloud.origin,
|
|
1521
|
+
await plane.bindSession(
|
|
1831
1522
|
workspace: '$clientId-onboard-scratch',
|
|
1832
1523
|
session: '$clientId-onboard-scratch',
|
|
1833
|
-
clientId: '$clientId-onboard',
|
|
1834
|
-
allowUnborn: true,
|
|
1835
1524
|
);
|
|
1836
1525
|
final kp = await plane.authorKeygen();
|
|
1837
|
-
|
|
1838
|
-
final secret = (kp['secret'] ?? '').toString();
|
|
1839
|
-
if (publicB64.isEmpty || secret.isEmpty) {
|
|
1526
|
+
if (kp.publicKey.length != 32 || kp.secret.length != 32) {
|
|
1840
1527
|
throw NomosTestHarnessException(
|
|
1841
1528
|
'cloudBound: authorKeygen returned no device key',
|
|
1842
1529
|
);
|
|
1843
1530
|
}
|
|
1531
|
+
final publicB64 = base64Encode(kp.publicKey);
|
|
1532
|
+
final secret = base64Encode(kp.secret);
|
|
1844
1533
|
final keyHash = sha256.convert(base64Decode(publicB64)).toString();
|
|
1845
1534
|
final minted = await post('/dev/mint-token', {
|
|
1846
1535
|
'sub': bareUid,
|
|
@@ -1859,16 +1548,16 @@ class NomosTestHarness {
|
|
|
1859
1548
|
// WITHOUT it the platform holds no delegation for the owner, the assembly falls to the
|
|
1860
1549
|
// no-delegation branch, the platform_cert lands in the delegation slot, and the home refuses at
|
|
1861
1550
|
// its OWN verify door ("home cert founder != delegation cert founder").
|
|
1862
|
-
final delegationCert = await plane.mintKeylessDelegation(
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1551
|
+
final delegationCert = await plane.mintKeylessDelegation(
|
|
1552
|
+
user: subject,
|
|
1553
|
+
devicePublic: kp.publicKey,
|
|
1554
|
+
childName: await plane.nmsReceivingGroupWorkspace(subject),
|
|
1555
|
+
parentId: platformWorkspace,
|
|
1556
|
+
verifiedVia: issuer,
|
|
1557
|
+
idpToken: idpToken,
|
|
1558
|
+
capturedClock: DateTime.now().toUtc().millisecondsSinceEpoch,
|
|
1559
|
+
deviceSecret: kp.secret,
|
|
1560
|
+
);
|
|
1872
1561
|
return (
|
|
1873
1562
|
publicB64: publicB64,
|
|
1874
1563
|
secret: secret,
|
|
@@ -1889,85 +1578,109 @@ class NomosTestHarness {
|
|
|
1889
1578
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
1890
1579
|
onDiagnostic: onDiagnostic,
|
|
1891
1580
|
run: () async {
|
|
1892
|
-
final homeName =
|
|
1581
|
+
final homeName = await plane.nmsReceivingGroupWorkspace(subject);
|
|
1893
1582
|
final bornAt = DateTime.now().toUtc().toIso8601String();
|
|
1894
|
-
final
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
);
|
|
1937
|
-
}
|
|
1583
|
+
final scratch = plane.session('$clientId-onboard-scratch');
|
|
1584
|
+
final enrolled = await scratch.nomosInternalOfferRemoteGeneratedCall(
|
|
1585
|
+
targetWorkspace: platformWorkspace,
|
|
1586
|
+
actor: subject,
|
|
1587
|
+
authorSecret: device.secret,
|
|
1588
|
+
authToken: device.idpToken,
|
|
1589
|
+
call: frameworkCalls.enrollFirstDevice(
|
|
1590
|
+
domain: applicationHomeDomain,
|
|
1591
|
+
workspace: platformWorkspace,
|
|
1592
|
+
keyHash: device.keyHash,
|
|
1593
|
+
verifiedVia: device.issuer,
|
|
1594
|
+
validFromEpoch: 0,
|
|
1595
|
+
grantedAt: bornAt,
|
|
1596
|
+
idpToken: device.idpToken,
|
|
1597
|
+
delegationCertificate: device.delegationCert,
|
|
1598
|
+
),
|
|
1599
|
+
);
|
|
1600
|
+
// The Home's OWN certificate: plain, device-signed, no keyless fields — the delegation
|
|
1601
|
+
// enrolled above rides the chain as its own link (see mintHomeBirthCertificate).
|
|
1602
|
+
final homeCert = await plane.mintHomeBirthCertificate(
|
|
1603
|
+
user: subject,
|
|
1604
|
+
devicePublic: base64Decode(device.publicB64),
|
|
1605
|
+
childName: homeName,
|
|
1606
|
+
parentId: platformWorkspace,
|
|
1607
|
+
deviceSecret: base64Decode(device.secret),
|
|
1608
|
+
);
|
|
1609
|
+
await scratch.nomosInternalOfferRemoteGeneratedCall(
|
|
1610
|
+
targetWorkspace: platformWorkspace,
|
|
1611
|
+
actor: subject,
|
|
1612
|
+
authorSecret: device.secret,
|
|
1613
|
+
authToken: device.idpToken,
|
|
1614
|
+
call: frameworkCalls.birthReceivingGroup(
|
|
1615
|
+
domain: applicationHomeDomain,
|
|
1616
|
+
workspace: platformWorkspace,
|
|
1617
|
+
owner: subject,
|
|
1618
|
+
frameworkHash: Sha256Digest(frameworkHash!),
|
|
1619
|
+
lawHash: Sha256Digest(applicationHomeLawHash),
|
|
1620
|
+
bornAt: bornAt,
|
|
1621
|
+
),
|
|
1622
|
+
birthCertificate: homeCert,
|
|
1623
|
+
requiredTargetHead: enrolled.head,
|
|
1624
|
+
);
|
|
1938
1625
|
home = homeName;
|
|
1939
1626
|
},
|
|
1940
1627
|
);
|
|
1941
|
-
// 6/6 —
|
|
1628
|
+
// 6/6 — bind the interaction, then observe the local verification postcondition.
|
|
1629
|
+
// Binding owns no custody. The cloud's published retained placement independently
|
|
1630
|
+
// drives Replica Follow; this test barrier observes readiness and never starts sync.
|
|
1942
1631
|
await _phased(
|
|
1943
1632
|
phase: '6/6: mount the born home',
|
|
1944
1633
|
workspace: home,
|
|
1945
|
-
awaiting: '
|
|
1634
|
+
awaiting: 'retained Home custody and local chain verification',
|
|
1946
1635
|
timeout: phaseTimeout ?? connectTimeout,
|
|
1947
1636
|
onDiagnostic: onDiagnostic,
|
|
1948
|
-
run: ()
|
|
1949
|
-
cloud: cloud.origin,
|
|
1950
|
-
workspace: home!,
|
|
1951
|
-
clientId: clientId,
|
|
1637
|
+
run: () async {
|
|
1952
1638
|
// A PRIVATE home (declared read caps — co2's shape) serves its ledger only to a VERIFIED
|
|
1953
1639
|
// principal: ride the caller's token when given, else the dev-IdP token the ceremony minted.
|
|
1954
|
-
|
|
1955
|
-
//
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1640
|
+
// The home was born WARRANTED with THIS device enrolled by the kernel-owned attestation step,
|
|
1641
|
+
// so the same key signs every later write and its own `.births()` offers.
|
|
1642
|
+
await plane.session('$clientId-onboard-scratch').refreshCredentials(
|
|
1643
|
+
authToken: authToken ?? device.idpToken,
|
|
1644
|
+
authorSecret: device.secret,
|
|
1645
|
+
);
|
|
1646
|
+
applicationInteraction = await plane.bindSession(
|
|
1647
|
+
workspace: home!,
|
|
1648
|
+
session: home!,
|
|
1649
|
+
credentialSourceSession: '$clientId-onboard-scratch',
|
|
1650
|
+
);
|
|
1651
|
+
plane.nomosInternalRegisterCredentialProvider(
|
|
1652
|
+
interaction: applicationInteraction,
|
|
1653
|
+
cloud: cloud.origin,
|
|
1654
|
+
signIn: ({deviceChallenge}) async => authToken ?? device.idpToken,
|
|
1655
|
+
);
|
|
1656
|
+
final deadline = DateTime.now().add(phaseTimeout ?? connectTimeout);
|
|
1657
|
+
while (true) {
|
|
1658
|
+
try {
|
|
1659
|
+
final verdict = await plane.session(home!).verifyChain();
|
|
1660
|
+
if (!verdict.valid) {
|
|
1661
|
+
throw NomosTestHarnessException(
|
|
1662
|
+
'cloudBound: born Home failed kernel verification: '
|
|
1663
|
+
'${verdict.hasError() ? verdict.error : 'unknown refusal'}',
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
break;
|
|
1667
|
+
} on NomosUnavailable catch (unavailable) {
|
|
1668
|
+
if (!unavailable.retryable ||
|
|
1669
|
+
!{
|
|
1670
|
+
NomosUnavailableReason.localReplicaNotReady,
|
|
1671
|
+
NomosUnavailableReason.workspaceOpening,
|
|
1672
|
+
}.contains(unavailable.reason) ||
|
|
1673
|
+
DateTime.now().isAfter(deadline)) rethrow;
|
|
1674
|
+
onDiagnostic?.call(
|
|
1675
|
+
'[nomos-test-harness] waiting for retained Home: ${unavailable.reason.name}',
|
|
1676
|
+
);
|
|
1677
|
+
await Future<void>.delayed(
|
|
1678
|
+
unavailable.retryAfter ?? const Duration(milliseconds: 100),
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1969
1683
|
);
|
|
1970
|
-
await applicationSession.autoSync(fallbackMs: 60000);
|
|
1971
1684
|
// NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
|
|
1972
1685
|
// locally-born child), so the plane is never sole-session — the decoy the offline harness
|
|
1973
1686
|
// needs (to break single-session inference) would just be a spurious local-only workspace here.
|
|
@@ -1994,7 +1707,10 @@ class NomosTestHarness {
|
|
|
1994
1707
|
entry: entry,
|
|
1995
1708
|
resolveFrom: resolveFrom,
|
|
1996
1709
|
node: node,
|
|
1997
|
-
|
|
1710
|
+
arguments: runnerArguments
|
|
1711
|
+
.where((arg) => !arg.startsWith('--custody='))
|
|
1712
|
+
.toList(),
|
|
1713
|
+
custodyDirectory: custodyDirectory,
|
|
1998
1714
|
cloud: cloud.origin,
|
|
1999
1715
|
authToken: authToken ?? device.idpToken,
|
|
2000
1716
|
authorSecret: device.secret,
|
|
@@ -2020,10 +1736,6 @@ class NomosTestHarness {
|
|
|
2020
1736
|
}
|
|
2021
1737
|
}
|
|
2022
1738
|
|
|
2023
|
-
/// Compiler/runtime-only receiving-group address. The law independently re-derives the same value.
|
|
2024
|
-
static String _receivingGroupNameOf(String principal) =>
|
|
2025
|
-
'nomos-rg-${sha256.convert(utf8.encode(principal)).toString().substring(0, 40)}';
|
|
2026
|
-
|
|
2027
1739
|
/// CONVERGE local custody with the test cloud (co2 topology step 8): sync every connected session —
|
|
2028
1740
|
/// PARENT-FIRST by connect order, so the home's chain lands remotely and its birth offer-effects create
|
|
2029
1741
|
/// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
|
|
@@ -2047,7 +1759,7 @@ class NomosTestHarness {
|
|
|
2047
1759
|
while (pending.isNotEmpty) {
|
|
2048
1760
|
for (final ws in List.of(pending)) {
|
|
2049
1761
|
try {
|
|
2050
|
-
final r = await
|
|
1762
|
+
final r = await _syncForTest(ws);
|
|
2051
1763
|
last[ws] = r;
|
|
2052
1764
|
lastErrors.remove(ws);
|
|
2053
1765
|
final permanentFailure = nmsPermanentConvergenceFailure(r);
|
|
@@ -2056,8 +1768,7 @@ class NomosTestHarness {
|
|
|
2056
1768
|
'converge: $ws rejected or dead-lettered business work — $permanentFailure',
|
|
2057
1769
|
);
|
|
2058
1770
|
}
|
|
2059
|
-
if (
|
|
2060
|
-
pending.remove(ws);
|
|
1771
|
+
if (nmsRemoteConvergenceSettled(r)) pending.remove(ws);
|
|
2061
1772
|
} on NomosTestHarnessException {
|
|
2062
1773
|
rethrow;
|
|
2063
1774
|
} catch (e) {
|
|
@@ -2076,12 +1787,119 @@ class NomosTestHarness {
|
|
|
2076
1787
|
return last;
|
|
2077
1788
|
}
|
|
2078
1789
|
|
|
1790
|
+
/// Test-only convergence control. Production application sessions cannot
|
|
1791
|
+
/// drive Replica Follow; cloud fixtures need an explicit completion barrier.
|
|
1792
|
+
Future<SyncWorkspaceResponse> _syncForTest(String workspace) async {
|
|
1793
|
+
final runtime = _transport as NomosRuntimeV2Transport;
|
|
1794
|
+
final response = await runtime.invokeRuntime(protocol.RuntimeRequest(
|
|
1795
|
+
protocolVersion: 2,
|
|
1796
|
+
requestId: Int64(runtime.nextRuntimeRequestId()),
|
|
1797
|
+
syncWorkspace: protocol.SyncWorkspaceRequest(
|
|
1798
|
+
workspace: workspace,
|
|
1799
|
+
admit: true,
|
|
1800
|
+
),
|
|
1801
|
+
));
|
|
1802
|
+
if (!response.hasSyncWorkspace()) {
|
|
1803
|
+
throw StateError(
|
|
1804
|
+
'Test convergence returned ${response.whichOutcome().name}'
|
|
1805
|
+
'${response.hasFailure() ? ': ${response.failure.code}: ${response.failure.message}' : ''}',
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
return response.syncWorkspace;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
2079
1811
|
// ── CUSTODY DIAGNOSTIC HELPERS (architecture/session_bound_clients.md ask 9) ──
|
|
2080
1812
|
|
|
2081
|
-
///
|
|
1813
|
+
/// Capture a point for a retained-upgrade fixture in this throwaway peer.
|
|
1814
|
+
Future<NomosTestFrontier> captureLocalFrontier(String workspace,
|
|
1815
|
+
{NomosTestFrontier? after}) async {
|
|
1816
|
+
if (_testPeer == null || testCloud != null) {
|
|
1817
|
+
throw StateError(
|
|
1818
|
+
'Retained frontier fixtures require an isolated local test peer.');
|
|
1819
|
+
}
|
|
1820
|
+
if (!_plane.boundSessions.contains(workspace)) {
|
|
1821
|
+
throw StateError('Capture requires an already-open local workspace.');
|
|
1822
|
+
}
|
|
1823
|
+
if (after != null) {
|
|
1824
|
+
_requireLocalFrontier(after);
|
|
1825
|
+
if (after.workspace != workspace)
|
|
1826
|
+
throw StateError('Fixture base belongs to another workspace.');
|
|
1827
|
+
}
|
|
1828
|
+
final runtime = _transport as NomosRuntimeV2Transport;
|
|
1829
|
+
final exported = await runtime.invokeRuntime(protocol.RuntimeRequest(
|
|
1830
|
+
protocolVersion: 2,
|
|
1831
|
+
requestId: Int64(runtime.nextRuntimeRequestId()),
|
|
1832
|
+
exportLiveOffers: protocol.ExportLiveOffersRequest(
|
|
1833
|
+
session: workspace, baseHead: after?.head ?? ''),
|
|
1834
|
+
));
|
|
1835
|
+
if (!exported.hasLiveOffers()) {
|
|
1836
|
+
// The office answers a refusal as a typed failure, not as an absent field. Reporting only
|
|
1837
|
+
// "not exported" threw that away and every caller — the bench bridge included — saw a
|
|
1838
|
+
// sentence that named neither the workspace nor the reason.
|
|
1839
|
+
final because = exported.hasFailure()
|
|
1840
|
+
? '${exported.failure.code}: ${exported.failure.message}'
|
|
1841
|
+
: 'the office answered ${exported.whichOutcome().name}';
|
|
1842
|
+
throw StateError(
|
|
1843
|
+
"Test frontier offers were not exported for '$workspace' — $because");
|
|
1844
|
+
}
|
|
1845
|
+
return NomosTestFrontier._(this, workspace,
|
|
1846
|
+
await _plane.session(workspace).head(), exported.liveOffers);
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/// Arrange a stale retained target before opening the presentation under test.
|
|
1850
|
+
/// The test process stops before selecting an earlier already-authenticated
|
|
1851
|
+
/// Git point. Restart uses ordinary authenticated custody open; production
|
|
1852
|
+
/// and connected peers are ineligible. This is fixture arrangement, not sync.
|
|
1853
|
+
Future<void> arrangeRetainedFrontier(NomosTestFrontier frontier) async {
|
|
1854
|
+
_requireLocalFrontier(frontier);
|
|
1855
|
+
await restart(retainedFixture: frontier);
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/// Deliver already retained Git history through ordinary offer-gated sync.
|
|
1859
|
+
/// Reception owns subscription invalidation; the test cannot fabricate rows.
|
|
1860
|
+
Future<String> admitRetainedAdvance(NomosTestFrontier frontier) async {
|
|
1861
|
+
_requireLocalFrontier(frontier);
|
|
1862
|
+
if (!frontier._offers.hasInline() ||
|
|
1863
|
+
frontier._offers.inline.offers.isEmpty) {
|
|
1864
|
+
throw StateError(
|
|
1865
|
+
'Capture the advancing frontier with its earlier fixture base.');
|
|
1866
|
+
}
|
|
1867
|
+
final runtime = _transport as NomosRuntimeV2Transport;
|
|
1868
|
+
final response = await runtime.invokeRuntime(protocol.RuntimeRequest(
|
|
1869
|
+
protocolVersion: 2,
|
|
1870
|
+
requestId: Int64(runtime.nextRuntimeRequestId()),
|
|
1871
|
+
target: protocol.InvocationTarget(
|
|
1872
|
+
workspace: frontier.workspace, session: frontier.workspace),
|
|
1873
|
+
admitCustodyOffers: frontier._offers,
|
|
1874
|
+
));
|
|
1875
|
+
if (!response.hasCustodyOffersAdmitted() ||
|
|
1876
|
+
response.custodyOffersAdmitted.sessions
|
|
1877
|
+
.expand((session) => session.intents)
|
|
1878
|
+
.length !=
|
|
1879
|
+
frontier._offers.inline.offers.length ||
|
|
1880
|
+
response.custodyOffersAdmitted.sessions
|
|
1881
|
+
.expand((session) => session.intents)
|
|
1882
|
+
.any((intent) => !intent.hasAdmitted() && !intent.hasSkipped()) ||
|
|
1883
|
+
await _plane.session(frontier.workspace).head() !=
|
|
1884
|
+
response.custodyOffersAdmitted.main) {
|
|
1885
|
+
throw StateError('Retained fixture advance was not admitted: $response');
|
|
1886
|
+
}
|
|
1887
|
+
return response.custodyOffersAdmitted.main;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
void _requireLocalFrontier(NomosTestFrontier frontier) {
|
|
1891
|
+
if (_testPeer == null ||
|
|
1892
|
+
testCloud != null ||
|
|
1893
|
+
!identical(frontier._owner, this)) {
|
|
1894
|
+
throw StateError(
|
|
1895
|
+
'Frontier belongs to another or non-isolated test peer.');
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/// The raw multi-workspace plane under [reception] (the session handle a single-workspace harness
|
|
2082
1900
|
/// hands out is a view on it).
|
|
2083
|
-
|
|
2084
|
-
final b =
|
|
1901
|
+
DartReception get _plane {
|
|
1902
|
+
final b = reception;
|
|
2085
1903
|
return b is NomosSession ? b.parent : b;
|
|
2086
1904
|
}
|
|
2087
1905
|
|
|
@@ -2094,52 +1912,56 @@ class NomosTestHarness {
|
|
|
2094
1912
|
final lane = _cloudRestartLane;
|
|
2095
1913
|
final ceremonySessions = <String>{
|
|
2096
1914
|
_decoyWorkspace,
|
|
1915
|
+
..._setupSessions,
|
|
2097
1916
|
if (lane != null) '${lane.clientId}-onboard-scratch',
|
|
2098
1917
|
if (lane != null) '${lane.clientId}-platform-author',
|
|
2099
1918
|
};
|
|
2100
|
-
return _plane.
|
|
1919
|
+
return _plane.boundSessions
|
|
2101
1920
|
.where((session) => !ceremonySessions.contains(session))
|
|
2102
1921
|
.toList();
|
|
2103
1922
|
}
|
|
2104
1923
|
|
|
2105
|
-
///
|
|
2106
|
-
///
|
|
2107
|
-
///
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
if (
|
|
1924
|
+
/// Cold-reopen a workspace from the same real Git custody directory.
|
|
1925
|
+
///
|
|
1926
|
+
/// This deliberately does not reset or deserialize custody. Git is the
|
|
1927
|
+
/// durable test state; reopening proves the Peer Office can remount it.
|
|
1928
|
+
Future<void> reopenWorkspaceCustody(String workspace) async {
|
|
1929
|
+
if (!_sessionNames.contains(workspace)) {
|
|
2111
1930
|
throw NomosTestHarnessException(
|
|
2112
|
-
'
|
|
2113
|
-
'seeded workspaces: ${_snapshots.keys.toList()}',
|
|
1931
|
+
'reopenWorkspaceCustody("$workspace"): workspace is not mounted',
|
|
2114
1932
|
);
|
|
2115
1933
|
}
|
|
2116
1934
|
await _phased(
|
|
2117
|
-
phase: '
|
|
1935
|
+
phase: 'reopenWorkspaceCustody: remount Git custody',
|
|
2118
1936
|
workspace: workspace,
|
|
2119
1937
|
awaiting: 'connect',
|
|
2120
1938
|
timeout: _phaseTimeout ?? const Duration(seconds: 60),
|
|
2121
1939
|
onDiagnostic: _onDiagnostic,
|
|
2122
|
-
run: ()
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
1940
|
+
run: () async {
|
|
1941
|
+
await _plane.bindSession(
|
|
1942
|
+
workspace: workspace,
|
|
1943
|
+
session: workspace,
|
|
1944
|
+
);
|
|
1945
|
+
final authorSecret = _deviceSecrets[workspace];
|
|
1946
|
+
if (authorSecret != null) {
|
|
1947
|
+
await _plane.session(workspace).refreshCredentials(
|
|
1948
|
+
authorSecret: authorSecret,
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
},
|
|
2129
1952
|
);
|
|
2130
1953
|
}
|
|
2131
1954
|
|
|
2132
|
-
///
|
|
2133
|
-
///
|
|
2134
|
-
/// then connects the workspace as its own session. Returns its [NomosSession].
|
|
1955
|
+
/// Seed a new workspace through the same Peer Office and kernel used by the
|
|
1956
|
+
/// rest of this test. The resulting state is ordinary Git custody.
|
|
2135
1957
|
Future<NomosSession> seedWorkspaceFromLawUsdz({
|
|
2136
1958
|
required String workspace,
|
|
2137
1959
|
required List<String> lawUsdzPaths,
|
|
2138
1960
|
}) async {
|
|
2139
|
-
final
|
|
2140
|
-
if (
|
|
1961
|
+
final peer = _testPeer;
|
|
1962
|
+
if (peer == null) {
|
|
2141
1963
|
throw NomosTestHarnessException(
|
|
2142
|
-
'seedWorkspaceFromLawUsdz
|
|
1964
|
+
'seedWorkspaceFromLawUsdz requires a local canonical test peer',
|
|
2143
1965
|
);
|
|
2144
1966
|
}
|
|
2145
1967
|
if (lawUsdzPaths.isEmpty) {
|
|
@@ -2147,46 +1969,21 @@ class NomosTestHarness {
|
|
|
2147
1969
|
'seedWorkspaceFromLawUsdz("$workspace"): pass the build/<name>.law.usdz paths',
|
|
2148
1970
|
);
|
|
2149
1971
|
}
|
|
2150
|
-
final
|
|
2151
|
-
|
|
2152
|
-
phase: 'seed: offline law install',
|
|
1972
|
+
final installed = await _phased(
|
|
1973
|
+
phase: 'seed: initialize Git custody and install law',
|
|
2153
1974
|
workspace: workspace,
|
|
2154
|
-
awaiting:
|
|
2155
|
-
? 'offline-install.ts'
|
|
2156
|
-
: 'githolon harness-install',
|
|
1975
|
+
awaiting: 'genesis offer + ordinary lifecycle offers',
|
|
2157
1976
|
timeout: _phaseTimeout ?? _kInstallDeadline,
|
|
2158
1977
|
onDiagnostic: _onDiagnostic,
|
|
2159
|
-
run: () =>
|
|
2160
|
-
node: lane.node,
|
|
2161
|
-
useInRepoScript: lane.useInRepoScript,
|
|
2162
|
-
inRepoScript: lane.inRepoScript,
|
|
2163
|
-
githolonCmd: lane.githolonCmd,
|
|
2164
|
-
lawUsdzPaths: lawUsdzPaths,
|
|
2165
|
-
workspace: workspace,
|
|
2166
|
-
snapshot: snap,
|
|
2167
|
-
runtimeDir: lane.runtimeDir,
|
|
2168
|
-
onDiagnostic: _onDiagnostic,
|
|
2169
|
-
),
|
|
2170
|
-
);
|
|
2171
|
-
final b64 = base64Encode(await File(snap).readAsBytes());
|
|
2172
|
-
_snapshots[workspace] = b64;
|
|
2173
|
-
_lawHashes[workspace] = ((info['lawHashes'] as List?) ?? const [])
|
|
2174
|
-
.map((e) => e.toString())
|
|
2175
|
-
.toList();
|
|
2176
|
-
await _phased(
|
|
2177
|
-
phase: 'seed: connect session',
|
|
2178
|
-
workspace: workspace,
|
|
2179
|
-
awaiting: 'connect',
|
|
2180
|
-
timeout: _phaseTimeout ?? const Duration(seconds: 60),
|
|
2181
|
-
onDiagnostic: _onDiagnostic,
|
|
2182
|
-
run: () => _plane.connect(
|
|
1978
|
+
run: () => peer.installWorkspace(
|
|
2183
1979
|
workspace: workspace,
|
|
2184
|
-
|
|
2185
|
-
clientId: '${lane.clientId}-$workspace',
|
|
2186
|
-
restoreFrom: b64,
|
|
2187
|
-
allowUnborn: true,
|
|
1980
|
+
lawUsdzPaths: lawUsdzPaths,
|
|
2188
1981
|
),
|
|
2189
1982
|
);
|
|
1983
|
+
_lawHashes[workspace] = installed.lawHashes;
|
|
1984
|
+
_domainKeys[workspace] = installed.domainKeys;
|
|
1985
|
+
_devicePublics[workspace] = base64Encode(installed.deviceKey.publicKey);
|
|
1986
|
+
_deviceSecrets[workspace] = base64Encode(installed.deviceKey.secret);
|
|
2190
1987
|
if (!_sessionNames.contains(workspace)) _sessionNames.add(workspace);
|
|
2191
1988
|
return _plane.session(workspace);
|
|
2192
1989
|
}
|
|
@@ -2199,15 +1996,7 @@ class NomosTestHarness {
|
|
|
2199
1996
|
required String workspace,
|
|
2200
1997
|
required String domain,
|
|
2201
1998
|
}) async {
|
|
2202
|
-
final
|
|
2203
|
-
phase: 'assertDomainInstalled: read the interface offer',
|
|
2204
|
-
workspace: workspace,
|
|
2205
|
-
awaiting: 'interfaceOffer',
|
|
2206
|
-
timeout: _phaseTimeout ?? _kOtherDeadline,
|
|
2207
|
-
onDiagnostic: _onDiagnostic,
|
|
2208
|
-
run: () => _plane.session(workspace).interfaceOffer(),
|
|
2209
|
-
);
|
|
2210
|
-
final keys = (offer?.keys.toList() ?? const <String>[])..sort();
|
|
1999
|
+
final keys = (_domainKeys[workspace]?.toList() ?? const <String>[])..sort();
|
|
2211
2000
|
if (!keys.contains(domain)) {
|
|
2212
2001
|
throw NomosTestHarnessException(
|
|
2213
2002
|
'domain "$domain" is NOT installed on workspace "$workspace" — active domains: '
|
|
@@ -2216,189 +2005,137 @@ class NomosTestHarness {
|
|
|
2216
2005
|
}
|
|
2217
2006
|
}
|
|
2218
2007
|
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
///
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
Future<WorkspaceSnapshot> snapshotWorkspace(String workspace) async {
|
|
2226
|
-
final connected = _plane.connectedSessions;
|
|
2227
|
-
if (!connected.contains(workspace)) {
|
|
2008
|
+
/// Reopen the application from durable LOCAL custody. The canonical runner spawned for a
|
|
2009
|
+
/// local scenario owns a filesystem Git directory independently of the Node process. Restarting
|
|
2010
|
+
/// replaces Reception, Peer Office and kernel memory, then reconnects the same workspace names
|
|
2011
|
+
/// against that directory. No snapshot or remote fetch participates.
|
|
2012
|
+
Future<void> restart({NomosTestFrontier? retainedFixture}) async {
|
|
2013
|
+
if (_disposed) {
|
|
2228
2014
|
throw NomosTestHarnessException(
|
|
2229
|
-
'
|
|
2015
|
+
'restart(): this test device has already been disposed',
|
|
2230
2016
|
);
|
|
2231
2017
|
}
|
|
2232
|
-
final
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
run: () async {
|
|
2240
|
-
final b64 = await s.export();
|
|
2241
|
-
final head = await s.head();
|
|
2242
|
-
return WorkspaceSnapshot(
|
|
2243
|
-
workspace: workspace,
|
|
2244
|
-
snapshotB64: b64,
|
|
2245
|
-
head: head,
|
|
2246
|
-
lawHashes: lawHashes(workspace),
|
|
2247
|
-
capturedAt: DateTime.now().toUtc(),
|
|
2018
|
+
final peer = _testPeer;
|
|
2019
|
+
final home = homeWorkspace;
|
|
2020
|
+
final cloudLane = _cloudRestartLane;
|
|
2021
|
+
if (cloudLane != null && home != null) {
|
|
2022
|
+
if (retainedFixture != null) {
|
|
2023
|
+
throw NomosTestHarnessException(
|
|
2024
|
+
'restart(): retained frontier fixtures require an isolated local test peer',
|
|
2248
2025
|
);
|
|
2249
|
-
}
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
/// Snapshot EVERY connected workspace on this plane into a typed [PlaneSnapshot] (session/workspace metadata
|
|
2254
|
-
/// carried per entry). The plane-level twin of [snapshotWorkspace] — migrate a whole staged AppSim plane.
|
|
2255
|
-
Future<PlaneSnapshot> snapshotPlane() async {
|
|
2256
|
-
final out = <String, WorkspaceSnapshot>{};
|
|
2257
|
-
final connected = _applicationConnectedSessions();
|
|
2258
|
-
for (final ws in connected) {
|
|
2259
|
-
out[ws] = await snapshotWorkspace(ws);
|
|
2260
|
-
}
|
|
2261
|
-
return PlaneSnapshot(
|
|
2262
|
-
workspaces: Map.unmodifiable(out),
|
|
2263
|
-
capturedAt: DateTime.now().toUtc(),
|
|
2264
|
-
);
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
/// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
|
|
2268
|
-
/// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
|
|
2269
|
-
/// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
|
|
2270
|
-
Future<NomosSession> restoreWorkspace(
|
|
2271
|
-
WorkspaceSnapshot snapshot, {
|
|
2272
|
-
String? into,
|
|
2273
|
-
}) async {
|
|
2274
|
-
final ws = into ?? snapshot.workspace;
|
|
2275
|
-
await _phased(
|
|
2276
|
-
phase: 'restoreWorkspace',
|
|
2277
|
-
workspace: ws,
|
|
2278
|
-
awaiting: 'connect(restoreFrom:)',
|
|
2279
|
-
timeout: _phaseTimeout ?? const Duration(seconds: 60),
|
|
2280
|
-
onDiagnostic: _onDiagnostic,
|
|
2281
|
-
run: () => _plane.connect(
|
|
2282
|
-
workspace: ws,
|
|
2283
|
-
session: ws,
|
|
2284
|
-
clientId: '${_installLane?.clientId ?? 'test-runner'}-$ws',
|
|
2285
|
-
restoreFrom: snapshot.snapshotB64,
|
|
2286
|
-
allowUnborn: true,
|
|
2287
|
-
),
|
|
2288
|
-
);
|
|
2289
|
-
if (!_sessionNames.contains(ws)) _sessionNames.add(ws);
|
|
2290
|
-
return bridge.session(ws);
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
/// Restore an entire [PlaneSnapshot] — every workspace re-opened from its snapshot. Migrates a staged plane
|
|
2294
|
-
/// wholesale (the mechanical AppSim-snapshot migration co2 #5 asks for).
|
|
2295
|
-
Future<void> restorePlane(PlaneSnapshot plane) async {
|
|
2296
|
-
for (final entry in plane.workspaces.values) {
|
|
2297
|
-
await restoreWorkspace(entry);
|
|
2026
|
+
}
|
|
2027
|
+
await _reopenCloudDevice(cloudLane, home);
|
|
2028
|
+
return;
|
|
2298
2029
|
}
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
/// Simulate an app process restart from its durably persisted local custody. This is deliberately one
|
|
2302
|
-
/// harness verb: snapshots, runner replacement, credentials, workspace ordering and auto-sync registration
|
|
2303
|
-
/// remain Nomos concerns. The caller reacquires generated clients from [homeSession]/[bridge] afterward.
|
|
2304
|
-
Future<void> restart() async {
|
|
2305
|
-
if (_disposed) {
|
|
2030
|
+
if (peer == null || home == null) {
|
|
2306
2031
|
throw NomosTestHarnessException(
|
|
2307
|
-
'restart()
|
|
2032
|
+
'restart() requires a local canonical test peer and primary application workspace',
|
|
2308
2033
|
);
|
|
2309
2034
|
}
|
|
2310
|
-
final
|
|
2311
|
-
final
|
|
2312
|
-
if (
|
|
2035
|
+
final publicKey = _devicePublics[home];
|
|
2036
|
+
final homeSecret = _deviceSecrets[home];
|
|
2037
|
+
if (publicKey == null ||
|
|
2038
|
+
publicKey.isEmpty ||
|
|
2039
|
+
homeSecret == null ||
|
|
2040
|
+
homeSecret.isEmpty) {
|
|
2313
2041
|
throw NomosTestHarnessException(
|
|
2314
|
-
'restart()
|
|
2042
|
+
'restart(): the local device has no retained signing identity for "$home"',
|
|
2315
2043
|
);
|
|
2316
2044
|
}
|
|
2317
|
-
|
|
2318
|
-
final oldPlane = _plane;
|
|
2319
2045
|
final targets = _applicationConnectedSessions();
|
|
2320
2046
|
if (targets.remove(home)) targets.insert(0, home);
|
|
2321
|
-
final
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
onDiagnostic: _onDiagnostic,
|
|
2330
|
-
run: session.stopAutoSync,
|
|
2331
|
-
);
|
|
2332
|
-
snapshots[ws] = await _phased(
|
|
2333
|
-
phase: 'restart: persist local custody',
|
|
2334
|
-
workspace: ws,
|
|
2335
|
-
awaiting: 'export',
|
|
2336
|
-
timeout: _phaseTimeout ?? _kOtherDeadline,
|
|
2337
|
-
onDiagnostic: _onDiagnostic,
|
|
2338
|
-
run: session.export,
|
|
2339
|
-
);
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2047
|
+
final secrets = <String, String>{
|
|
2048
|
+
for (final workspace in targets)
|
|
2049
|
+
workspace: _deviceSecrets[workspace] ?? homeSecret,
|
|
2050
|
+
};
|
|
2051
|
+
final prior = retainedFixture == null
|
|
2052
|
+
? null
|
|
2053
|
+
: await _plane.session(retainedFixture.workspace).head();
|
|
2054
|
+
if (retainedFixture != null) _requireLocalFrontier(retainedFixture);
|
|
2342
2055
|
await _phased(
|
|
2343
|
-
phase: 'restart:
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
run: _transport.dispose,
|
|
2348
|
-
);
|
|
2349
|
-
_transport = await _phased(
|
|
2350
|
-
phase: 'restart: spawn runner',
|
|
2351
|
-
awaiting: 'Process.start(${lane.node})',
|
|
2352
|
-
timeout: _phaseTimeout ?? _kSpawnDeadline,
|
|
2056
|
+
phase: 'restart: reopen Peer Office from Git custody',
|
|
2057
|
+
workspace: home,
|
|
2058
|
+
awaiting: 'process reboot + custody remount',
|
|
2059
|
+
timeout: _phaseTimeout ?? _kInstallDeadline,
|
|
2353
2060
|
onDiagnostic: _onDiagnostic,
|
|
2354
|
-
run: () =>
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2061
|
+
run: () => peer.reboot(
|
|
2062
|
+
trustRootPublicKey: base64Decode(publicKey),
|
|
2063
|
+
authorSecrets: secrets,
|
|
2064
|
+
arrangeStoppedCustody: retainedFixture == null
|
|
2065
|
+
? null
|
|
2066
|
+
: () async {
|
|
2067
|
+
final fixture = retainedFixture;
|
|
2068
|
+
// Exact existing workspace beneath this peer-owned temporary root.
|
|
2069
|
+
if (!RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(fixture.workspace)) {
|
|
2070
|
+
throw StateError('Invalid fixture workspace');
|
|
2071
|
+
}
|
|
2072
|
+
final repo =
|
|
2073
|
+
'${peer.custody.directory.path}/custody/holon-${fixture.workspace}.git';
|
|
2074
|
+
// `update-ref <new> <old>` is a compare-and-swap, and git reads an
|
|
2075
|
+
// EMPTY <old> as "the ref must not exist". `prior` comes from the
|
|
2076
|
+
// plane's workspace status, which answers '' whenever the workspace
|
|
2077
|
+
// is not resident at that moment — after a page crawl has released
|
|
2078
|
+
// it, for instance — so the rewind failed with "cannot lock ref
|
|
2079
|
+
// 'refs/heads/main': reference already exists". The peer is stopped
|
|
2080
|
+
// and this repo is its own temporary custody, so what the repo holds
|
|
2081
|
+
// IS the truth: use it whenever the plane had no answer.
|
|
2082
|
+
var expected = prior ?? '';
|
|
2083
|
+
if (expected.isEmpty) {
|
|
2084
|
+
final current = await Process.run('git', [
|
|
2085
|
+
'--git-dir=$repo',
|
|
2086
|
+
'rev-parse',
|
|
2087
|
+
'--verify',
|
|
2088
|
+
'refs/heads/main',
|
|
2089
|
+
]);
|
|
2090
|
+
if (current.exitCode != 0) {
|
|
2091
|
+
throw StateError(
|
|
2092
|
+
'Could not read the retained Git point to rewind from: ${current.stderr}');
|
|
2093
|
+
}
|
|
2094
|
+
expected = (current.stdout as String).trim();
|
|
2095
|
+
}
|
|
2096
|
+
final selected = await Process.run('git', [
|
|
2097
|
+
'--git-dir=$repo',
|
|
2098
|
+
'update-ref',
|
|
2099
|
+
'refs/heads/main',
|
|
2100
|
+
fixture.head,
|
|
2101
|
+
expected,
|
|
2102
|
+
]);
|
|
2103
|
+
if (selected.exitCode != 0)
|
|
2104
|
+
throw StateError(
|
|
2105
|
+
'Could not arrange isolated retained Git point: ${selected.stderr}');
|
|
2106
|
+
},
|
|
2360
2107
|
),
|
|
2361
2108
|
);
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
phase: 'restart: runner ready',
|
|
2365
|
-
awaiting: "event:'ready'",
|
|
2366
|
-
timeout: _phaseTimeout ?? _kOtherDeadline,
|
|
2367
|
-
onDiagnostic: _onDiagnostic,
|
|
2368
|
-
run: () => restarted.ready,
|
|
2369
|
-
);
|
|
2370
|
-
for (final ws in targets) {
|
|
2371
|
-
await _phased(
|
|
2372
|
-
phase: 'restart: reopen local custody',
|
|
2373
|
-
workspace: ws,
|
|
2374
|
-
awaiting: 'connect(restoreFrom:)',
|
|
2375
|
-
timeout: _phaseTimeout ?? const Duration(seconds: 60),
|
|
2376
|
-
onDiagnostic: _onDiagnostic,
|
|
2377
|
-
run: () => restarted.connect(
|
|
2378
|
-
cloud: lane.cloud,
|
|
2379
|
-
workspace: ws,
|
|
2380
|
-
session: ws,
|
|
2381
|
-
clientId: '${lane.clientId}-$ws',
|
|
2382
|
-
authToken: lane.authToken,
|
|
2383
|
-
authorSecret: lane.authorSecret,
|
|
2384
|
-
restoreFrom: snapshots[ws],
|
|
2385
|
-
allowUnborn: true,
|
|
2386
|
-
),
|
|
2387
|
-
);
|
|
2388
|
-
}
|
|
2389
|
-
bridge = restarted.session(home);
|
|
2109
|
+
_transport = peer.transport;
|
|
2110
|
+
reception = peer.reception.session(home);
|
|
2390
2111
|
_sessionNames
|
|
2391
2112
|
..clear()
|
|
2392
2113
|
..addAll(targets);
|
|
2393
|
-
_snapshots.addAll(snapshots);
|
|
2394
|
-
for (final ws in targets) {
|
|
2395
|
-
await restarted.session(ws).autoSync(fallbackMs: 60000);
|
|
2396
|
-
}
|
|
2397
2114
|
}
|
|
2398
2115
|
|
|
2399
|
-
///
|
|
2400
|
-
///
|
|
2401
|
-
///
|
|
2116
|
+
/// The scenario-side delegated-permission lane (a test device minting a delegation cert and
|
|
2117
|
+
/// authoring the acceptance intent on the person's behalf) was retired with the local-tooling
|
|
2118
|
+
/// cutover (2026-09-03): it was host-side orchestration outside the one Peer Office. Generated
|
|
2119
|
+
/// `may…()` helpers still call here; the honest answer is a typed refusal naming the intent, so
|
|
2120
|
+
/// the scenario establishes authority through the generated business API instead.
|
|
2121
|
+
Future<void> nomosInternalAllowDelegatedScenarioAction({
|
|
2122
|
+
required String domain,
|
|
2123
|
+
required String acceptanceIntent,
|
|
2124
|
+
required String relation,
|
|
2125
|
+
required String subject,
|
|
2126
|
+
required String subjectField,
|
|
2127
|
+
required String certificateField,
|
|
2128
|
+
Map<String, Object?> facts = const {},
|
|
2129
|
+
}) async {
|
|
2130
|
+
throw NomosTestHarnessException(
|
|
2131
|
+
'delegated scenario permissions are not available in the canonical harness: '
|
|
2132
|
+
'establish "$relation" for $subject through the generated business API '
|
|
2133
|
+
'(domain $domain, acceptance intent $acceptanceIntent)',
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
/// Reinstall with empty local custody; subsequent reads must reacquire the
|
|
2138
|
+
/// remotely retained frontier. Restart, in contrast, preserves device disk.
|
|
2402
2139
|
Future<void> reinstall() async {
|
|
2403
2140
|
if (_disposed) {
|
|
2404
2141
|
throw NomosTestHarnessException(
|
|
@@ -2413,43 +2150,69 @@ class NomosTestHarness {
|
|
|
2413
2150
|
);
|
|
2414
2151
|
}
|
|
2415
2152
|
|
|
2416
|
-
|
|
2153
|
+
// LOCAL-FIRST REINSTALL: a fresh install restores from the cloud, so what survives is exactly what
|
|
2154
|
+
// this peer had SYNCED — the Home's births are re-admitted by the cloud through its own gate (which
|
|
2155
|
+
// births the children there), then each child's own work follows. Converge first, parent leading;
|
|
2156
|
+
// an unsynced push failure surfaces here instead of as a silent loss after the restart.
|
|
2157
|
+
await converge();
|
|
2158
|
+
// createTemp's argument is the PREFIX; the receiver is the parent directory.
|
|
2159
|
+
final freshCustody = await _workdir.createTemp('reinstall-');
|
|
2160
|
+
await _reopenCloudDevice(lane, home, custody: freshCustody.path);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
/// Replace only the process and its bindings. Placement/follow scheduling
|
|
2164
|
+
/// stays in Peer Office; the harness supplies credentials, never sync flags.
|
|
2165
|
+
Future<void> _reopenCloudDevice(
|
|
2166
|
+
_CloudRestartLane lane,
|
|
2167
|
+
String home, {
|
|
2168
|
+
String? custody,
|
|
2169
|
+
}) async {
|
|
2417
2170
|
final targets = _applicationConnectedSessions();
|
|
2418
2171
|
if (targets.remove(home)) targets.insert(0, home);
|
|
2419
|
-
|
|
2420
|
-
await oldPlane.session(ws).stopAutoSync();
|
|
2421
|
-
}
|
|
2172
|
+
await _plane.dispose();
|
|
2422
2173
|
await _transport.dispose();
|
|
2423
2174
|
|
|
2424
2175
|
_transport = await NodeRunnerTransport.start(
|
|
2425
2176
|
entry: lane.entry,
|
|
2426
2177
|
resolveFrom: lane.resolveFrom,
|
|
2427
2178
|
node: lane.node,
|
|
2428
|
-
|
|
2179
|
+
arguments: [
|
|
2180
|
+
...lane.arguments,
|
|
2181
|
+
'--custody=${custody ?? lane.custodyDirectory}'
|
|
2182
|
+
],
|
|
2429
2183
|
onDiagnostic: _onDiagnostic,
|
|
2430
2184
|
);
|
|
2431
|
-
|
|
2185
|
+
if (custody != null) lane.custodyDirectory = custody;
|
|
2186
|
+
final reinstalled = DartReception(_transport);
|
|
2432
2187
|
await reinstalled.ready;
|
|
2188
|
+
final scratch = '${lane.clientId}-onboard-scratch';
|
|
2189
|
+
await reinstalled.bindSession(
|
|
2190
|
+
workspace: scratch,
|
|
2191
|
+
session: scratch,
|
|
2192
|
+
);
|
|
2193
|
+
await reinstalled.session(scratch).refreshCredentials(
|
|
2194
|
+
authToken: lane.authToken,
|
|
2195
|
+
authorSecret: lane.authorSecret,
|
|
2196
|
+
);
|
|
2433
2197
|
for (final ws in targets) {
|
|
2434
|
-
await reinstalled.
|
|
2435
|
-
cloud: lane.cloud,
|
|
2198
|
+
final interaction = await reinstalled.bindSession(
|
|
2436
2199
|
workspace: ws,
|
|
2437
2200
|
session: ws,
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2201
|
+
credentialSourceSession: scratch,
|
|
2202
|
+
);
|
|
2203
|
+
reinstalled.nomosInternalRegisterCredentialProvider(
|
|
2204
|
+
interaction: interaction,
|
|
2205
|
+
cloud: lane.cloud,
|
|
2206
|
+
signIn: ({deviceChallenge}) async => lane.authToken,
|
|
2441
2207
|
);
|
|
2442
2208
|
}
|
|
2443
|
-
|
|
2209
|
+
reception = reinstalled.session(home);
|
|
2444
2210
|
_sessionNames
|
|
2445
2211
|
..clear()
|
|
2446
2212
|
..addAll(targets);
|
|
2447
|
-
for (final ws in targets) {
|
|
2448
|
-
await reinstalled.session(ws).autoSync(fallbackMs: 60000);
|
|
2449
|
-
}
|
|
2450
2213
|
}
|
|
2451
2214
|
|
|
2452
|
-
/// Deterministic, idempotent, total-ordered teardown: close the
|
|
2215
|
+
/// Deterministic, idempotent, total-ordered teardown: close the reception and all live watches, stop the runner
|
|
2453
2216
|
/// (SIGTERM → 5s deadline → SIGKILL, via [NodeRunnerTransport.dispose]), then delete the temp dir. Safe to
|
|
2454
2217
|
/// call twice; safe to call after a failed setup. Nothing left running, nothing left on disk.
|
|
2455
2218
|
/// Disposal must ALWAYS complete (never leave a hung teardown holding the test runner): the transport
|
|
@@ -2464,21 +2227,21 @@ class NomosTestHarness {
|
|
|
2464
2227
|
'[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …',
|
|
2465
2228
|
);
|
|
2466
2229
|
try {
|
|
2467
|
-
// The
|
|
2230
|
+
// The reception owns the transport's one message subscription and every watch controller. Closing the
|
|
2468
2231
|
// transport first makes its incoming stream wait on consumers that can no longer receive their
|
|
2469
2232
|
// unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
|
|
2470
2233
|
await _plane.dispose().timeout(
|
|
2471
2234
|
_phaseTimeout ?? _kOtherDeadline,
|
|
2472
2235
|
onTimeout: () => throw HarnessPhaseTimeout(
|
|
2473
|
-
phase: 'dispose: close
|
|
2236
|
+
phase: 'dispose: close reception watches',
|
|
2474
2237
|
workspace: workspace,
|
|
2475
2238
|
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
2476
|
-
awaiting: '
|
|
2239
|
+
awaiting: 'DartReception.dispose',
|
|
2477
2240
|
),
|
|
2478
2241
|
);
|
|
2479
2242
|
} catch (e) {
|
|
2480
2243
|
_diag(
|
|
2481
|
-
'[nomos-test-harness]
|
|
2244
|
+
'[nomos-test-harness] reception dispose error (continuing to runner cleanup): $e',
|
|
2482
2245
|
);
|
|
2483
2246
|
}
|
|
2484
2247
|
try {
|
|
@@ -2513,79 +2276,46 @@ class NomosTestHarness {
|
|
|
2513
2276
|
}
|
|
2514
2277
|
}
|
|
2515
2278
|
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
required List<String> lawUsdzPaths,
|
|
2524
|
-
required String workspace,
|
|
2525
|
-
required String snapshot,
|
|
2526
|
-
String? runtimeDir,
|
|
2527
|
-
void Function(String line)? onDiagnostic,
|
|
2528
|
-
}) async {
|
|
2529
|
-
// Both lanes take the SAME flags (--law/--workspace/--out/--runtime) and print the SAME one-line JSON
|
|
2530
|
-
// contract; only the leading command differs (node <script> vs `<githolon...> harness-install`).
|
|
2531
|
-
final common = <String>[
|
|
2532
|
-
'--law',
|
|
2533
|
-
lawUsdzPaths.join(','),
|
|
2534
|
-
'--workspace',
|
|
2535
|
-
workspace,
|
|
2536
|
-
'--out',
|
|
2537
|
-
snapshot,
|
|
2538
|
-
if (runtimeDir != null) ...['--runtime', runtimeDir],
|
|
2539
|
-
];
|
|
2540
|
-
final String executable;
|
|
2541
|
-
final List<String> args;
|
|
2542
|
-
if (useInRepoScript) {
|
|
2543
|
-
executable = node;
|
|
2544
|
-
args = [inRepoScript, ...common];
|
|
2545
|
-
} else {
|
|
2546
|
-
// `githolonCmd` is a command line (e.g. `npx githolon` or `node /abs/cli.mjs`) — split into tokens.
|
|
2547
|
-
final tokens = githolonCmd
|
|
2548
|
-
.trim()
|
|
2549
|
-
.split(RegExp(r'\s+'))
|
|
2550
|
-
.where((t) => t.isNotEmpty)
|
|
2551
|
-
.toList();
|
|
2552
|
-
if (tokens.isEmpty) {
|
|
2553
|
-
throw NomosTestHarnessException(
|
|
2554
|
-
'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)',
|
|
2279
|
+
static Future<String> _resolveTestEntry(String node, String from) async {
|
|
2280
|
+
String? packageConfigPath = Platform.packageConfig;
|
|
2281
|
+
if (packageConfigPath == null) {
|
|
2282
|
+
var directory = Directory(from).absolute;
|
|
2283
|
+
while (true) {
|
|
2284
|
+
final candidate = File(
|
|
2285
|
+
'${directory.path}${Platform.pathSeparator}.dart_tool${Platform.pathSeparator}package_config.json',
|
|
2555
2286
|
);
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (err.isNotEmpty) {
|
|
2564
|
-
for (final line in err.split('\n')) {
|
|
2565
|
-
(onDiagnostic ?? stderr.writeln)('[$tag] $line');
|
|
2287
|
+
if (candidate.existsSync()) {
|
|
2288
|
+
packageConfigPath = candidate.uri.toString();
|
|
2289
|
+
break;
|
|
2290
|
+
}
|
|
2291
|
+
final parent = directory.parent;
|
|
2292
|
+
if (parent.path == directory.path) break;
|
|
2293
|
+
directory = parent;
|
|
2566
2294
|
}
|
|
2567
2295
|
}
|
|
2568
|
-
if (
|
|
2569
|
-
|
|
2570
|
-
|
|
2296
|
+
if (packageConfigPath != null) {
|
|
2297
|
+
final packages = await loadPackageConfigUri(
|
|
2298
|
+
Uri.base.resolve(packageConfigPath),
|
|
2571
2299
|
);
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
final out = (r.stdout as String).trim();
|
|
2575
|
-
final last = out.isEmpty ? '' : out.split('\n').last;
|
|
2576
|
-
Map<String, Object?> decoded = const {};
|
|
2577
|
-
try {
|
|
2578
|
-
final d = jsonDecode(last);
|
|
2579
|
-
if (d is Map) decoded = d.cast<String, Object?>();
|
|
2580
|
-
} catch (_) {}
|
|
2581
|
-
if (decoded['ok'] != true || !File(snapshot).existsSync()) {
|
|
2582
|
-
throw NomosTestHarnessException(
|
|
2583
|
-
'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})',
|
|
2300
|
+
final bundled = packages.resolve(
|
|
2301
|
+
Uri.parse('package:nomos_client/src/runner/node-entry.mjs'),
|
|
2584
2302
|
);
|
|
2303
|
+
if (bundled != null && bundled.scheme == 'file') {
|
|
2304
|
+
final path = bundled.toFilePath();
|
|
2305
|
+
if (File(path).existsSync()) return path;
|
|
2306
|
+
}
|
|
2585
2307
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2308
|
+
final result = await Process.run(
|
|
2309
|
+
node,
|
|
2310
|
+
['-p', "require.resolve('@githolon/runtime/runner/node-entry.mjs')"],
|
|
2311
|
+
workingDirectory: from,
|
|
2312
|
+
);
|
|
2313
|
+
final resolved = result.stdout.toString().trim();
|
|
2314
|
+
if (result.exitCode == 0 && resolved.isNotEmpty) return resolved;
|
|
2315
|
+
throw NomosTestHarnessException(
|
|
2316
|
+
'the Nomos release contains no bundled node peer. '
|
|
2317
|
+
'Build @githolon/runtime, reinstall nomos_client, or pass nodeEntry only while developing Nomos.',
|
|
2318
|
+
);
|
|
2589
2319
|
}
|
|
2590
2320
|
|
|
2591
2321
|
static Future<String> _resolveEntry(String node, String from) async {
|
|
@@ -2619,10 +2349,10 @@ class NomosTestHarness {
|
|
|
2619
2349
|
}
|
|
2620
2350
|
// PREFER the BUILT .mjs (the published-consumer entry): node refuses type-stripping under node_modules
|
|
2621
2351
|
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — co2 DEFECT P3-002), so the raw .ts entry only boots
|
|
2622
|
-
// in-repo. The .mjs ships from @githolon/
|
|
2352
|
+
// in-repo. The .mjs ships from @githolon/runtime ≥0.97.2; the .ts fallback keeps in-repo dev working.
|
|
2623
2353
|
for (final entry in const [
|
|
2624
|
-
'@githolon/
|
|
2625
|
-
'@githolon/
|
|
2354
|
+
'@githolon/runtime/runner/node-entry.mjs',
|
|
2355
|
+
'@githolon/runtime/runner/node-entry.ts',
|
|
2626
2356
|
]) {
|
|
2627
2357
|
final r = await Process.run(
|
|
2628
2358
|
node,
|
|
@@ -2636,82 +2366,8 @@ class NomosTestHarness {
|
|
|
2636
2366
|
}
|
|
2637
2367
|
throw NomosTestHarnessException(
|
|
2638
2368
|
'the Nomos release contains no bundled node runner and '
|
|
2639
|
-
'@githolon/
|
|
2369
|
+
'@githolon/runtime/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
|
|
2640
2370
|
'Reinstall nomos_client, or pass nodeEntry: only when developing Nomos itself.',
|
|
2641
2371
|
);
|
|
2642
2372
|
}
|
|
2643
2373
|
}
|
|
2644
|
-
|
|
2645
|
-
/// A typed, self-describing snapshot of ONE workspace's local holon (co2 #5). [snapshotB64] is the exact
|
|
2646
|
-
/// base64 `connect(restoreFrom:)` consumes — the whole nomos.git incl. un-synced writes + the DLQ — plus the
|
|
2647
|
-
/// metadata that makes a staged snapshot mechanically migratable: the [workspace], its ledger [head] at
|
|
2648
|
-
/// capture, its installed [lawHashes], and [capturedAt] (UTC).
|
|
2649
|
-
class WorkspaceSnapshot {
|
|
2650
|
-
final String workspace;
|
|
2651
|
-
final String snapshotB64;
|
|
2652
|
-
final String head;
|
|
2653
|
-
final List<String> lawHashes;
|
|
2654
|
-
final DateTime capturedAt;
|
|
2655
|
-
|
|
2656
|
-
const WorkspaceSnapshot({
|
|
2657
|
-
required this.workspace,
|
|
2658
|
-
required this.snapshotB64,
|
|
2659
|
-
required this.head,
|
|
2660
|
-
required this.lawHashes,
|
|
2661
|
-
required this.capturedAt,
|
|
2662
|
-
});
|
|
2663
|
-
|
|
2664
|
-
/// JSON round-trip so a staged snapshot persists to disk verbatim (the bytes + provenance).
|
|
2665
|
-
Map<String, Object?> toJson() => {
|
|
2666
|
-
'workspace': workspace,
|
|
2667
|
-
'snapshotB64': snapshotB64,
|
|
2668
|
-
'head': head,
|
|
2669
|
-
'lawHashes': lawHashes,
|
|
2670
|
-
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
2671
|
-
};
|
|
2672
|
-
|
|
2673
|
-
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
2674
|
-
WorkspaceSnapshot(
|
|
2675
|
-
workspace: j['workspace'] as String,
|
|
2676
|
-
snapshotB64: j['snapshotB64'] as String,
|
|
2677
|
-
head: (j['head'] ?? '') as String,
|
|
2678
|
-
lawHashes:
|
|
2679
|
-
((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
2680
|
-
capturedAt: DateTime.parse(
|
|
2681
|
-
(j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
|
|
2682
|
-
as String,
|
|
2683
|
-
),
|
|
2684
|
-
);
|
|
2685
|
-
|
|
2686
|
-
@override
|
|
2687
|
-
String toString() =>
|
|
2688
|
-
'WorkspaceSnapshot($workspace @ ${head.isEmpty ? "∅" : head.substring(0, head.length.clamp(0, 10))}, '
|
|
2689
|
-
'${lawHashes.length} law(s), ${capturedAt.toIso8601String()})';
|
|
2690
|
-
}
|
|
2691
|
-
|
|
2692
|
-
/// A typed snapshot of a WHOLE plane — every connected workspace keyed by name (co2 #5). Restore it with
|
|
2693
|
-
/// [NomosTestHarness.restorePlane] to migrate a staged multi-workspace AppSim scene wholesale.
|
|
2694
|
-
class PlaneSnapshot {
|
|
2695
|
-
final Map<String, WorkspaceSnapshot> workspaces;
|
|
2696
|
-
final DateTime capturedAt;
|
|
2697
|
-
|
|
2698
|
-
const PlaneSnapshot({required this.workspaces, required this.capturedAt});
|
|
2699
|
-
|
|
2700
|
-
Map<String, Object?> toJson() => {
|
|
2701
|
-
'workspaces': workspaces.map((k, v) => MapEntry(k, v.toJson())),
|
|
2702
|
-
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
2703
|
-
};
|
|
2704
|
-
|
|
2705
|
-
factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
|
|
2706
|
-
workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
|
|
2707
|
-
(k, v) => MapEntry(
|
|
2708
|
-
'$k',
|
|
2709
|
-
WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
|
|
2710
|
-
),
|
|
2711
|
-
),
|
|
2712
|
-
capturedAt: DateTime.parse(
|
|
2713
|
-
(j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
|
|
2714
|
-
as String,
|
|
2715
|
-
),
|
|
2716
|
-
);
|
|
2717
|
-
}
|