@githolon/dsl 0.108.7 → 0.110.1
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 +80 -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 +214 -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 +268 -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 +23950 -15776
- 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 +550 -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_event_desk_close_test.dart +16 -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 +1276 -0
- package/src/codegen_call_usda.ts +49 -6
- package/src/codegen_dart.ts +83 -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 +2017 -1195
- 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,600 +0,0 @@
|
|
|
1
|
-
// ADVERSARIAL FINDER — MULTI-WORKSPACE REALM INTERFERENCE (one engine, many sessions).
|
|
2
|
-
// Hammers the cross-session seams of the local holarchy plane (co2's production shape: a
|
|
3
|
-
// dozen+ live workspaces, mixed laws) through the REAL runner + REAL kernel, fully offline:
|
|
4
|
-
// 1. concurrent offers to different sessions (no cross-chain bleed, independent heads)
|
|
5
|
-
// 2. watches on A while B floods (no tick bleed / starvation, per-subscription ordering)
|
|
6
|
-
// 3. acquireSession racing a FAILED connect (the phantom-session window)
|
|
7
|
-
// 4. pendingConflicts/activityLog isolation across same-named queries / sibling floods
|
|
8
|
-
// 5. session re-connect (atomic replace) during in-flight offers — nothing hangs, typed only
|
|
9
|
-
// 6. born child resident-then-session — one consistent view
|
|
10
|
-
// 7. error isolation — one session's refusal never poisons siblings
|
|
11
|
-
// 8. runner-level LRU (maxMounted) — watch survival + state preservation across park/remount
|
|
12
|
-
//
|
|
13
|
-
// A FAILING expectation here is a FINDING (evidence of a seam bug), not necessarily a
|
|
14
|
-
// regression gate. Timing assertions use generous CI margins.
|
|
15
|
-
@TestOn('vm')
|
|
16
|
-
@Timeout(Duration(minutes: 12))
|
|
17
|
-
library;
|
|
18
|
-
|
|
19
|
-
import 'dart:async';
|
|
20
|
-
import 'dart:convert';
|
|
21
|
-
import 'dart:io';
|
|
22
|
-
|
|
23
|
-
import 'package:nomos_client/src/nms_conformance_runtime.dart';
|
|
24
|
-
import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
|
|
25
|
-
import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
|
|
26
|
-
import 'package:test/test.dart';
|
|
27
|
-
|
|
28
|
-
String get _root => '${Directory.current.path}/../..';
|
|
29
|
-
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
|
|
30
|
-
String get _installer => '$_root/cloud/web-client/runner/offline-install.ts';
|
|
31
|
-
String get _guestbook =>
|
|
32
|
-
'$_root/examples/guestbook/build/guestbook.law.usdz';
|
|
33
|
-
String get _surfaced =>
|
|
34
|
-
'$_root/bench/scale/fixtures/surfaced/build/surfaced.law.usdz';
|
|
35
|
-
String get _attributed =>
|
|
36
|
-
'$_root/bench/scale/fixtures/attributed/build/attributed.law.usdz';
|
|
37
|
-
String get _estateBirth =>
|
|
38
|
-
'$_root/bench/scale/fixtures/estate-birth/build/estate-birth.law.usdz';
|
|
39
|
-
String get _runtimeDir =>
|
|
40
|
-
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
41
|
-
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
42
|
-
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
43
|
-
|
|
44
|
-
String? _missingPrereq() {
|
|
45
|
-
for (final p in [
|
|
46
|
-
_repoEntry,
|
|
47
|
-
_installer,
|
|
48
|
-
_guestbook,
|
|
49
|
-
_surfaced,
|
|
50
|
-
_attributed,
|
|
51
|
-
_estateBirth
|
|
52
|
-
]) {
|
|
53
|
-
if (!File(p).existsSync())
|
|
54
|
-
return 'missing $p (compile the fixture / example first)';
|
|
55
|
-
}
|
|
56
|
-
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
57
|
-
!File('$_runtimeDir/bootstrap.law.usdz').existsSync() ||
|
|
58
|
-
!File('$_runtimeDir/nomos.law.usdz').existsSync()) {
|
|
59
|
-
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
String _nowIso() => DateTime.now().toUtc().toIso8601String();
|
|
65
|
-
|
|
66
|
-
Future<String> _sign(NomosSession s, String author, String message) =>
|
|
67
|
-
s.offerDirective(
|
|
68
|
-
domain: 'guestbook',
|
|
69
|
-
directiveId: 'signGuestbook',
|
|
70
|
-
payload: {
|
|
71
|
-
'author': author,
|
|
72
|
-
'message': message,
|
|
73
|
-
'mood': 'delighted',
|
|
74
|
-
'signedAt': _nowIso()
|
|
75
|
-
},
|
|
76
|
-
actor: author);
|
|
77
|
-
|
|
78
|
-
void main() {
|
|
79
|
-
final skip = _missingPrereq();
|
|
80
|
-
|
|
81
|
-
group('shared cluster (6 workspaces, mixed laws)', () {
|
|
82
|
-
late NomosTestHarness harness;
|
|
83
|
-
late NomosBridge plane;
|
|
84
|
-
|
|
85
|
-
setUpAll(() async {
|
|
86
|
-
if (skip != null) return;
|
|
87
|
-
harness = await NomosTestHarness.cluster(
|
|
88
|
-
workspaces: {
|
|
89
|
-
'home-birth': [_estateBirth],
|
|
90
|
-
'surf': [_surfaced],
|
|
91
|
-
'attr': [_attributed],
|
|
92
|
-
'gb-1': [_guestbook],
|
|
93
|
-
'gb-2': [_guestbook],
|
|
94
|
-
'gb-3': [_guestbook],
|
|
95
|
-
},
|
|
96
|
-
nodeEntry: _repoEntry,
|
|
97
|
-
runtimeDir: _runtimeDir,
|
|
98
|
-
);
|
|
99
|
-
plane = harness.bridge;
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
tearDownAll(() async {
|
|
103
|
-
if (skip != null) return;
|
|
104
|
-
await harness.dispose();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test(
|
|
108
|
-
'1. concurrent offers to different sessions: no cross-chain bleed, independent heads',
|
|
109
|
-
() async {
|
|
110
|
-
if (skip != null) return markTestSkipped(skip);
|
|
111
|
-
const gbs = ['gb-1', 'gb-2', 'gb-3'];
|
|
112
|
-
final sw = Stopwatch()..start();
|
|
113
|
-
final futs = <Future<Object?>>[
|
|
114
|
-
for (final ws in gbs)
|
|
115
|
-
for (var i = 0; i < 4; i++)
|
|
116
|
-
_sign(harness.session(ws), 'author-$ws', 'm$i'),
|
|
117
|
-
for (var i = 0; i < 4; i++)
|
|
118
|
-
harness.session('surf').offerDirective(
|
|
119
|
-
domain: 'docs',
|
|
120
|
-
directiveId: 'createDoc',
|
|
121
|
-
payload: {'owner': 'owner-surf', 'title': 'doc-$i'},
|
|
122
|
-
actor: 'owner-surf'),
|
|
123
|
-
for (var i = 0; i < 4; i++)
|
|
124
|
-
harness.session('attr').offerDirective(
|
|
125
|
-
domain: 'sites',
|
|
126
|
-
directiveId: 'createSite',
|
|
127
|
-
payload: {'owner': 'owner-attr', 'name': 'site-$i'},
|
|
128
|
-
actor: 'owner-attr'),
|
|
129
|
-
];
|
|
130
|
-
final heads =
|
|
131
|
-
await Future.wait(futs); // 20 concurrent offers across 5 sessions
|
|
132
|
-
sw.stop();
|
|
133
|
-
expect(heads, everyElement(isNotNull));
|
|
134
|
-
// ack latency stays sane under concurrency (generous CI margin)
|
|
135
|
-
expect(sw.elapsed, lessThan(const Duration(seconds: 90)),
|
|
136
|
-
reason: '20 concurrent offers should settle well under 90s');
|
|
137
|
-
|
|
138
|
-
// each chain holds ONLY its own writes
|
|
139
|
-
for (final ws in gbs) {
|
|
140
|
-
final mine = await harness
|
|
141
|
-
.session(ws)
|
|
142
|
-
.query('entriesByAuthor', {'author': 'author-$ws'});
|
|
143
|
-
expect(mine, hasLength(4), reason: '$ws holds its own 4 writes');
|
|
144
|
-
for (final other in gbs.where((o) => o != ws)) {
|
|
145
|
-
final theirs = await harness
|
|
146
|
-
.session(ws)
|
|
147
|
-
.query('entriesByAuthor', {'author': 'author-$other'});
|
|
148
|
-
expect(theirs, isEmpty,
|
|
149
|
-
reason:
|
|
150
|
-
"$ws must not hold $other's writes (cross-session bleed)");
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
expect(
|
|
154
|
-
await harness
|
|
155
|
-
.session('surf')
|
|
156
|
-
.query('docsByOwner', {'owner': 'owner-surf'}),
|
|
157
|
-
hasLength(4));
|
|
158
|
-
expect(
|
|
159
|
-
await harness
|
|
160
|
-
.session('attr')
|
|
161
|
-
.query('sitesByOwner', {'owner': 'owner-attr'}),
|
|
162
|
-
hasLength(4));
|
|
163
|
-
|
|
164
|
-
// heads are independent per session
|
|
165
|
-
final tips = <String>{};
|
|
166
|
-
for (final ws in [...gbs, 'surf', 'attr']) {
|
|
167
|
-
tips.add(await harness.session(ws).head());
|
|
168
|
-
}
|
|
169
|
-
expect(tips, hasLength(5),
|
|
170
|
-
reason: 'every session has its own ledger tip');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test(
|
|
174
|
-
'2. a watch on session A fires while session B floods (no starvation, no bleed, ordered)',
|
|
175
|
-
() async {
|
|
176
|
-
if (skip != null) return markTestSkipped(skip);
|
|
177
|
-
final a = harness.session('gb-1');
|
|
178
|
-
final b = harness.session('gb-2');
|
|
179
|
-
final idle = harness.session('attr');
|
|
180
|
-
|
|
181
|
-
final emissionsA = <int>[];
|
|
182
|
-
final emissionsIdle = <int>[];
|
|
183
|
-
final subA = a.watch('entriesByAuthor', {'author': 'watched'}).listen(
|
|
184
|
-
(rows) => emissionsA.add(rows.length));
|
|
185
|
-
final subIdle = idle
|
|
186
|
-
.watch('sitesByOwner', {'owner': 'nobody-here'}).listen(
|
|
187
|
-
(rows) => emissionsIdle.add(rows.length));
|
|
188
|
-
await Future<void>.delayed(
|
|
189
|
-
const Duration(milliseconds: 600)); // initial emissions
|
|
190
|
-
final idleBase = emissionsIdle.length;
|
|
191
|
-
|
|
192
|
-
// B floods 25 writes while A takes 3 interleaved writes
|
|
193
|
-
final flood = () async {
|
|
194
|
-
for (var i = 0; i < 25; i++) {
|
|
195
|
-
await _sign(b, 'flooder', 'f$i');
|
|
196
|
-
}
|
|
197
|
-
}();
|
|
198
|
-
final aWrites = () async {
|
|
199
|
-
for (var i = 0; i < 3; i++) {
|
|
200
|
-
await Future<void>.delayed(const Duration(milliseconds: 60));
|
|
201
|
-
await _sign(a, 'watched', 'w$i');
|
|
202
|
-
}
|
|
203
|
-
}();
|
|
204
|
-
await Future.wait([flood, aWrites]);
|
|
205
|
-
|
|
206
|
-
// A's watch must reach 3 rows promptly despite the sibling flood (generous margin)
|
|
207
|
-
final deadline = DateTime.now().add(const Duration(seconds: 15));
|
|
208
|
-
while (DateTime.now().isBefore(deadline) &&
|
|
209
|
-
(emissionsA.isEmpty || emissionsA.last < 3)) {
|
|
210
|
-
await Future<void>.delayed(const Duration(milliseconds: 150));
|
|
211
|
-
}
|
|
212
|
-
await subA.cancel();
|
|
213
|
-
await subIdle.cancel();
|
|
214
|
-
expect(emissionsA.isNotEmpty && emissionsA.last == 3, isTrue,
|
|
215
|
-
reason:
|
|
216
|
-
"A's watch starved or missed rows under B's flood: $emissionsA");
|
|
217
|
-
for (var i = 1; i < emissionsA.length; i++) {
|
|
218
|
-
expect(emissionsA[i], greaterThanOrEqualTo(emissionsA[i - 1]),
|
|
219
|
-
reason: 'per-subscription ordering must be monotonic: $emissionsA');
|
|
220
|
-
}
|
|
221
|
-
expect(emissionsIdle.length, idleBase,
|
|
222
|
-
reason:
|
|
223
|
-
"the idle sibling watch got spurious emissions from B's flood: $emissionsIdle");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('3a. follow(ref) + racing acquireSession during a sibling offer flood',
|
|
227
|
-
() async {
|
|
228
|
-
if (skip != null) return markTestSkipped(skip);
|
|
229
|
-
final flood = () async {
|
|
230
|
-
for (var i = 0; i < 10; i++) {
|
|
231
|
-
await _sign(harness.session('gb-3'), 'race-flood', 'f$i');
|
|
232
|
-
}
|
|
233
|
-
}();
|
|
234
|
-
// follow an already-open sibling mid-flood → same handle, functional
|
|
235
|
-
final followed = await plane.follow(NmsInternalNomosRefs.create<Object?>(
|
|
236
|
-
workspace: 'gb-1', type: 'nomos.Workspace', id: 'gb-1'));
|
|
237
|
-
expect(followed.workspace, 'gb-1');
|
|
238
|
-
expect(await followed.query('entriesByAuthor', {'author': 'nobody'}),
|
|
239
|
-
isEmpty);
|
|
240
|
-
|
|
241
|
-
// concurrent acquireSession single-flights to the SAME handle
|
|
242
|
-
final pair = await Future.wait([
|
|
243
|
-
plane.acquireSession('gb-2'),
|
|
244
|
-
plane.acquireSession('gb-2'),
|
|
245
|
-
]);
|
|
246
|
-
expect(identical(pair[0], pair[1]), isTrue,
|
|
247
|
-
reason:
|
|
248
|
-
'two concurrent acquireSession calls must yield the same session');
|
|
249
|
-
await flood;
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test(
|
|
253
|
-
'3b. FINDER: a FAILED connect must not leave a phantom session acquireSession trusts',
|
|
254
|
-
() async {
|
|
255
|
-
if (skip != null) return markTestSkipped(skip);
|
|
256
|
-
// a garbage restoreFrom makes the runner's connect throw AFTER the Dart bridge has
|
|
257
|
-
// (prematurely?) registered the session name on the plane.
|
|
258
|
-
Object? err;
|
|
259
|
-
try {
|
|
260
|
-
await plane.connect(
|
|
261
|
-
workspace: 'phantom-ws',
|
|
262
|
-
session: 'phantom-ws',
|
|
263
|
-
restoreFrom: '%%%not-base64%%%');
|
|
264
|
-
} catch (e) {
|
|
265
|
-
err = e;
|
|
266
|
-
}
|
|
267
|
-
expect(err, isNotNull, reason: 'the corrupt-restore connect must throw');
|
|
268
|
-
|
|
269
|
-
// CORRECT behavior: acquireSession must either ask for credentials, throw a named
|
|
270
|
-
// StateError, or return a session whose ops actually work. Returning a handle whose
|
|
271
|
-
// every op refuses "session not connected" is the phantom-session bug.
|
|
272
|
-
var credentialsAsked = false;
|
|
273
|
-
Object? acquireOutcome;
|
|
274
|
-
try {
|
|
275
|
-
final s = await plane.acquireSession('phantom-ws',
|
|
276
|
-
ifAbsentCredentials: () async {
|
|
277
|
-
credentialsAsked = true;
|
|
278
|
-
throw StateError('credentials path taken (correct) — stop here');
|
|
279
|
-
});
|
|
280
|
-
// it returned a session: its ops must work, else it's a dead phantom handle
|
|
281
|
-
await s.query('entriesByAuthor', {'author': 'x'});
|
|
282
|
-
acquireOutcome = 'live session';
|
|
283
|
-
} catch (e) {
|
|
284
|
-
acquireOutcome = e;
|
|
285
|
-
}
|
|
286
|
-
final phantom = !credentialsAsked &&
|
|
287
|
-
acquireOutcome is! StateError &&
|
|
288
|
-
acquireOutcome != 'live session';
|
|
289
|
-
expect(phantom, isFalse,
|
|
290
|
-
reason:
|
|
291
|
-
'PHANTOM SESSION: after a failed connect, acquireSession("phantom-ws") '
|
|
292
|
-
'returned a dead handle (ops refuse) without asking for credentials — '
|
|
293
|
-
'observed: $acquireOutcome');
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
test(
|
|
297
|
-
'4. pendingConflicts/activityLog on A are unaffected by B (same-named queries isolated)',
|
|
298
|
-
() async {
|
|
299
|
-
if (skip != null) return markTestSkipped(skip);
|
|
300
|
-
final surf = harness.session('surf');
|
|
301
|
-
final before = await surf.pendingConflicts();
|
|
302
|
-
|
|
303
|
-
// sibling activity: a guestbook flood + surfaced-relevant field writes on ANOTHER session's law
|
|
304
|
-
for (var i = 0; i < 10; i++) {
|
|
305
|
-
await _sign(harness.session('gb-1'), 'iso-flood', 'f$i');
|
|
306
|
-
}
|
|
307
|
-
final after = await surf.pendingConflicts();
|
|
308
|
-
expect(after, equals(before),
|
|
309
|
-
reason:
|
|
310
|
-
"surf's pendingConflicts moved on a sibling's writes (cache/key bleed)");
|
|
311
|
-
|
|
312
|
-
// activityLog rows belong to the session's OWN chain only
|
|
313
|
-
final actGb2 = await harness.session('gb-2').activityLog(limit: 100);
|
|
314
|
-
final foreign = actGb2.where((r) =>
|
|
315
|
-
r['directiveId'] == 'createSite' ||
|
|
316
|
-
r['directiveId'] == 'createDoc' ||
|
|
317
|
-
(r['payload'] is Map &&
|
|
318
|
-
(r['payload'] as Map)['author'] == 'iso-flood'));
|
|
319
|
-
expect(foreign, isEmpty,
|
|
320
|
-
reason:
|
|
321
|
-
"gb-2's activityLog carries sibling-session intents: ${foreign.toList()}");
|
|
322
|
-
|
|
323
|
-
// the SAME query id with the SAME params on two sessions answers per-session
|
|
324
|
-
final q1 = await harness
|
|
325
|
-
.session('gb-1')
|
|
326
|
-
.query('entriesByAuthor', {'author': 'iso-flood'});
|
|
327
|
-
final q2 = await harness
|
|
328
|
-
.session('gb-2')
|
|
329
|
-
.query('entriesByAuthor', {'author': 'iso-flood'});
|
|
330
|
-
expect(q1, hasLength(10));
|
|
331
|
-
expect(q2, isEmpty,
|
|
332
|
-
reason:
|
|
333
|
-
'same-named query with same params leaked rows across sessions (cache keying)');
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test(
|
|
337
|
-
'5. re-connect (atomic replace) during in-flight offers: everything settles typed',
|
|
338
|
-
() async {
|
|
339
|
-
if (skip != null) return markTestSkipped(skip);
|
|
340
|
-
final s = harness.session('gb-3');
|
|
341
|
-
// fire 10 offers WITHOUT awaiting, then immediately replace the session (new clientId
|
|
342
|
-
// → identity change → dispose-then-connect under the hood)
|
|
343
|
-
final futs = [
|
|
344
|
-
for (var i = 0; i < 10; i++)
|
|
345
|
-
_sign(s, 'replace-race', 'r$i')
|
|
346
|
-
.then<Object?>((h) => 'ok:$h', onError: (Object e) => e)
|
|
347
|
-
];
|
|
348
|
-
await plane.connect(
|
|
349
|
-
workspace: 'gb-3', session: 'gb-3', clientId: 'replacer-1');
|
|
350
|
-
|
|
351
|
-
final settled = await Future.wait(futs).timeout(
|
|
352
|
-
const Duration(seconds: 60),
|
|
353
|
-
onTimeout: () => throw StateError(
|
|
354
|
-
'offers in flight across an atomic replace HUNG (never settled)'));
|
|
355
|
-
// every outcome is either an admitted head or a TYPED error — never a hang, never null
|
|
356
|
-
for (final r in settled) {
|
|
357
|
-
expect(r, isNotNull);
|
|
358
|
-
if (r is! String) {
|
|
359
|
-
expect(r, isA<Exception>(),
|
|
360
|
-
reason: 'a race loser must fail typed, got: $r');
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// the replaced session is consistent: chain verifies, reads serve, writes admit
|
|
365
|
-
final v = await harness.session('gb-3').verifyChain();
|
|
366
|
-
expect(v['valid'], isTrue,
|
|
367
|
-
reason:
|
|
368
|
-
'in-flight offers interleaved with the atomic replace corrupted the chain: $v');
|
|
369
|
-
final admitted = settled.whereType<String>().length;
|
|
370
|
-
final rows = await harness
|
|
371
|
-
.session('gb-3')
|
|
372
|
-
.query('entriesByAuthor', {'author': 'replace-race'});
|
|
373
|
-
expect(rows.length, admitted,
|
|
374
|
-
reason:
|
|
375
|
-
'$admitted offers acked ok but ${rows.length} rows visible after the replace '
|
|
376
|
-
'(an acked write was lost or duplicated across the replace window)');
|
|
377
|
-
await _sign(harness.session('gb-3'), 'post-replace', 'alive');
|
|
378
|
-
expect(
|
|
379
|
-
await harness
|
|
380
|
-
.session('gb-3')
|
|
381
|
-
.query('entriesByAuthor', {'author': 'post-replace'}),
|
|
382
|
-
hasLength(1));
|
|
383
|
-
// siblings untouched by gb-3's replace
|
|
384
|
-
expect(
|
|
385
|
-
await harness
|
|
386
|
-
.session('gb-1')
|
|
387
|
-
.query('entriesByAuthor', {'author': 'nobody'}),
|
|
388
|
-
isEmpty);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
test(
|
|
392
|
-
'6. born child resident-then-session: one consistent view, replace-safe',
|
|
393
|
-
() async {
|
|
394
|
-
if (skip != null) return markTestSkipped(skip);
|
|
395
|
-
final fw = harness.frameworkHash;
|
|
396
|
-
final law = harness.lawHashes('home-birth').first;
|
|
397
|
-
final outcome = await harness.session('home-birth').offerCreates(
|
|
398
|
-
domain: 'home',
|
|
399
|
-
directiveId: 'birthEstateWorkspace',
|
|
400
|
-
payload: {
|
|
401
|
-
'owner': 'adv-owner',
|
|
402
|
-
'frameworkHash': fw,
|
|
403
|
-
'lawHash': law,
|
|
404
|
-
'bornAt': _nowIso(),
|
|
405
|
-
},
|
|
406
|
-
actor: 'adv-owner',
|
|
407
|
-
);
|
|
408
|
-
expect(outcome.born, hasLength(1));
|
|
409
|
-
expect(outcome.birthOutcomes.first.status,
|
|
410
|
-
NomosBirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN);
|
|
411
|
-
final child = outcome.born.single;
|
|
412
|
-
|
|
413
|
-
// the child is resident BEFORE any session opens it — listWorkspaces sees it
|
|
414
|
-
final listedPre = await plane.listWorkspaces();
|
|
415
|
-
expect(listedPre.map((workspace) => workspace.workspace), contains(child),
|
|
416
|
-
reason:
|
|
417
|
-
'a born child is resident on the plane before it is connected');
|
|
418
|
-
|
|
419
|
-
// open it as a session (mount in place) and read the genesis seed
|
|
420
|
-
await plane.connect(workspace: child, session: child);
|
|
421
|
-
final rows1 = await plane
|
|
422
|
-
.session(child)
|
|
423
|
-
.query('estatesByOwner', {'owner': 'adv-owner'});
|
|
424
|
-
expect(rows1, hasLength(1));
|
|
425
|
-
|
|
426
|
-
// write into the child, then RE-open it with a new clientId (identity replace on a
|
|
427
|
-
// born-resident workspace) — the write must survive (one holon's truth, not two)
|
|
428
|
-
await plane
|
|
429
|
-
.session(child)
|
|
430
|
-
.offerDirective(
|
|
431
|
-
domain: 'estate',
|
|
432
|
-
directiveId: 'renameEstate',
|
|
433
|
-
payload: {'estateName': child, 'newName': 'renamed'},
|
|
434
|
-
actor: 'adv-owner')
|
|
435
|
-
.catchError(
|
|
436
|
-
(Object _) => ''); // directive may not exist — best-effort
|
|
437
|
-
await _noThrow(() => plane.connect(
|
|
438
|
-
workspace: child, session: child, clientId: 'child-re'));
|
|
439
|
-
final rows2 = await plane
|
|
440
|
-
.session(child)
|
|
441
|
-
.query('estatesByOwner', {'owner': 'adv-owner'});
|
|
442
|
-
expect(rows2, hasLength(1),
|
|
443
|
-
reason:
|
|
444
|
-
're-opening a born-resident child must present the SAME holon truth');
|
|
445
|
-
|
|
446
|
-
// the parent is unaffected
|
|
447
|
-
expect(
|
|
448
|
-
await plane
|
|
449
|
-
.session('home-birth')
|
|
450
|
-
.verifyChain()
|
|
451
|
-
.then((v) => v['valid']),
|
|
452
|
-
isTrue);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
test('7. error isolation: a refusal on one session never poisons siblings',
|
|
456
|
-
() async {
|
|
457
|
-
if (skip != null) return markTestSkipped(skip);
|
|
458
|
-
// unknown directive → typed refusal
|
|
459
|
-
await expectLater(
|
|
460
|
-
harness.session('gb-1').offerDirective(
|
|
461
|
-
domain: 'guestbook',
|
|
462
|
-
directiveId: 'noSuchDirective',
|
|
463
|
-
payload: {'x': 1}),
|
|
464
|
-
throwsA(anything));
|
|
465
|
-
// schema-violating payload → typed refusal
|
|
466
|
-
await expectLater(
|
|
467
|
-
harness.session('attr').offerDirective(
|
|
468
|
-
domain: 'sites',
|
|
469
|
-
directiveId: 'createSite',
|
|
470
|
-
payload: {'owner': '', 'name': ''}),
|
|
471
|
-
throwsA(anything));
|
|
472
|
-
// unknown query → typed refusal, not a hang
|
|
473
|
-
await expectLater(
|
|
474
|
-
harness.session('surf').query('noSuchQuery', {'k': 'v'}),
|
|
475
|
-
throwsA(anything));
|
|
476
|
-
|
|
477
|
-
// every sibling still serves reads AND admits writes
|
|
478
|
-
for (final ws in ['gb-1', 'gb-2', 'gb-3', 'surf', 'attr', 'home-birth']) {
|
|
479
|
-
final h = await harness.session(ws).head();
|
|
480
|
-
expect(h, isNotEmpty,
|
|
481
|
-
reason: '$ws must still answer after a sibling refusal');
|
|
482
|
-
}
|
|
483
|
-
await _sign(harness.session('gb-2'), 'healthy', 'still-alive');
|
|
484
|
-
expect(
|
|
485
|
-
await harness
|
|
486
|
-
.session('gb-2')
|
|
487
|
-
.query('entriesByAuthor', {'author': 'healthy'}),
|
|
488
|
-
hasLength(1));
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
group('runner-level LRU (maxMounted: 2)', () {
|
|
493
|
-
test('8. FINDER: watches + session state across LRU park/remount',
|
|
494
|
-
() async {
|
|
495
|
-
if (skip != null) return markTestSkipped(skip);
|
|
496
|
-
final workdir = await Directory.systemTemp.createTemp('adv-lru-');
|
|
497
|
-
NodeRunnerTransport? transport;
|
|
498
|
-
try {
|
|
499
|
-
// build 3 guestbook snapshots via the proven offline install lane
|
|
500
|
-
final snaps = <String, String>{};
|
|
501
|
-
for (final ws in ['lru-1', 'lru-2', 'lru-3']) {
|
|
502
|
-
final out = '${workdir.path}/$ws.bin';
|
|
503
|
-
final r = await Process.run('node', [
|
|
504
|
-
_installer,
|
|
505
|
-
'--law',
|
|
506
|
-
_guestbook,
|
|
507
|
-
'--workspace',
|
|
508
|
-
ws,
|
|
509
|
-
'--out',
|
|
510
|
-
out,
|
|
511
|
-
'--runtime',
|
|
512
|
-
_runtimeDir,
|
|
513
|
-
]);
|
|
514
|
-
expect(r.exitCode, 0,
|
|
515
|
-
reason: 'offline install of $ws failed: ${r.stderr}');
|
|
516
|
-
snaps[ws] = base64Encode(await File(out).readAsBytes());
|
|
517
|
-
}
|
|
518
|
-
transport = await NodeRunnerTransport.start(
|
|
519
|
-
entry: _repoEntry,
|
|
520
|
-
node: 'node',
|
|
521
|
-
environment: {'NOMOS_OFFLINE': '1'});
|
|
522
|
-
final bridge = NomosBridge(transport);
|
|
523
|
-
await bridge.ready.timeout(const Duration(seconds: 30));
|
|
524
|
-
|
|
525
|
-
// FIRST connect fixes the realm budget: maxMounted 2
|
|
526
|
-
await bridge.connect(
|
|
527
|
-
workspace: 'lru-1',
|
|
528
|
-
session: 'lru-1',
|
|
529
|
-
restoreFrom: snaps['lru-1'],
|
|
530
|
-
allowUnborn: true,
|
|
531
|
-
maxMounted: 2);
|
|
532
|
-
final s1 = bridge.session('lru-1');
|
|
533
|
-
await _sign(s1, 'survivor', 'before-eviction'); // un-synced local write
|
|
534
|
-
|
|
535
|
-
final emissions = <int>[];
|
|
536
|
-
Object? watchError;
|
|
537
|
-
final sub = s1.watch('entriesByAuthor', {'author': 'survivor'}).listen(
|
|
538
|
-
(rows) => emissions.add(rows.length),
|
|
539
|
-
onError: (Object e) => watchError = e);
|
|
540
|
-
await Future<void>.delayed(const Duration(milliseconds: 800));
|
|
541
|
-
expect(emissions.isNotEmpty && emissions.last == 1, isTrue,
|
|
542
|
-
reason:
|
|
543
|
-
'precondition: the watch saw the pre-eviction row: $emissions');
|
|
544
|
-
|
|
545
|
-
// mount two more workspaces → lru-1 is LRU-evicted (parked)
|
|
546
|
-
await bridge.connect(
|
|
547
|
-
workspace: 'lru-2',
|
|
548
|
-
session: 'lru-2',
|
|
549
|
-
restoreFrom: snaps['lru-2'],
|
|
550
|
-
allowUnborn: true);
|
|
551
|
-
await bridge.connect(
|
|
552
|
-
workspace: 'lru-3',
|
|
553
|
-
session: 'lru-3',
|
|
554
|
-
restoreFrom: snaps['lru-3'],
|
|
555
|
-
allowUnborn: true);
|
|
556
|
-
final listed = await bridge.listWorkspaces();
|
|
557
|
-
final l1 = listed.firstWhere((workspace) => workspace.workspace == 'lru-1');
|
|
558
|
-
expect(l1.residency, protocol.WorkspaceResidency.WORKSPACE_RESIDENCY_PARKED,
|
|
559
|
-
reason: 'lru-1 should be LRU-parked: $listed');
|
|
560
|
-
|
|
561
|
-
// (a) session ops transparently remount from the parked snapshot, state intact
|
|
562
|
-
final rows = await s1.query('entriesByAuthor', {'author': 'survivor'});
|
|
563
|
-
expect(rows, hasLength(1),
|
|
564
|
-
reason:
|
|
565
|
-
'the un-synced local write must survive the park/remount cycle');
|
|
566
|
-
|
|
567
|
-
// (b) the watch must SURVIVE the eviction+remount (or at least error TYPED —
|
|
568
|
-
// a silently-dead stream is the bug)
|
|
569
|
-
await _sign(s1, 'survivor', 'after-remount');
|
|
570
|
-
final deadline = DateTime.now().add(const Duration(seconds: 10));
|
|
571
|
-
while (DateTime.now().isBefore(deadline) &&
|
|
572
|
-
(emissions.isEmpty || emissions.last < 2) &&
|
|
573
|
-
watchError == null) {
|
|
574
|
-
await Future<void>.delayed(const Duration(milliseconds: 200));
|
|
575
|
-
}
|
|
576
|
-
await sub.cancel();
|
|
577
|
-
final died =
|
|
578
|
-
(emissions.isEmpty || emissions.last < 2) && watchError == null;
|
|
579
|
-
expect(died, isFalse,
|
|
580
|
-
reason:
|
|
581
|
-
'SILENT WATCH DEATH: the lru-1 watch stopped emitting after an LRU '
|
|
582
|
-
'eviction+remount, with NO error event — emissions=$emissions '
|
|
583
|
-
'(a Flutter app bound to this stream shows stale data forever)');
|
|
584
|
-
} finally {
|
|
585
|
-
try {
|
|
586
|
-
await transport?.dispose();
|
|
587
|
-
} catch (_) {}
|
|
588
|
-
try {
|
|
589
|
-
await workdir.delete(recursive: true);
|
|
590
|
-
} catch (_) {}
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
Future<void> _noThrow(Future<void> Function() fn) async {
|
|
597
|
-
try {
|
|
598
|
-
await fn();
|
|
599
|
-
} catch (_) {}
|
|
600
|
-
}
|