@githolon/dsl 0.101.6 → 0.102.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/dart/CHANGELOG.md +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- package/src/framework/identity.ts +0 -537
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for headless app tests. One call
|
|
2
|
-
// installs a tenant's compiled law into a FRESH LOCAL
|
|
3
|
-
//
|
|
2
|
+
// installs a tenant's compiled law into a FRESH LOCAL application parent (no cloud contact, ever), performs
|
|
3
|
+
// the same receiving-group birth a production application performs, boots the REAL githolon (the
|
|
4
|
+
// byte-identical wasm, the real ledger, the real gate) over [NodeRunnerTransport], and hands
|
|
4
5
|
// out a [NomosBridge] the generated Dart clients drive — the SAME surface the app UI uses — under
|
|
5
6
|
// `flutter test` / `dart test` / CI.
|
|
6
7
|
//
|
|
@@ -16,29 +17,69 @@
|
|
|
16
17
|
// ```
|
|
17
18
|
//
|
|
18
19
|
// HOW IT STAYS OFFLINE. Two node children, both talking only to the local disk:
|
|
19
|
-
// 1. `offline-install.ts` runs the PROVEN offline install lane (`githolon proof`'s adapter engine)
|
|
20
|
-
// genesis → framework controller → each tenant package's
|
|
21
|
-
//
|
|
20
|
+
// 1. `offline-install.ts` runs the PROVEN offline install lane (`githolon proof`'s adapter engine) on a
|
|
21
|
+
// throwaway application parent — genesis → framework controller → each tenant package's
|
|
22
|
+
// `installDomain`, folded through the ONE gate — and serializes its tree to a fresh temp dir. Generated
|
|
23
|
+
// scenarios then birth their deterministic receiving group locally through the real declared birth.
|
|
22
24
|
// 2. the node runner (`node-entry.ts`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
|
|
23
25
|
// local cache (`~/.holon/runtime`, filled by any warm `githolon` run) and boots the holon from that
|
|
24
26
|
// snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
|
|
25
27
|
//
|
|
26
|
-
// PREREQUISITES (documented so a failure is legible): `node` on PATH
|
|
27
|
-
//
|
|
28
|
-
//
|
|
28
|
+
// PREREQUISITES (documented so a failure is legible): `node` on PATH and the release-pinned runtime cache.
|
|
29
|
+
// The self-contained runner + installer ship inside nomos_client; the kernel executable does not. A cold
|
|
30
|
+
// runtime is acquired and content-addressed by the release tooling, then tests remain fully offline.
|
|
29
31
|
//
|
|
30
32
|
// PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
|
|
31
33
|
// runner. For a PUBLISHED harness this install belongs behind a `githolon` subcommand (the CLI already
|
|
32
34
|
// bundles the engine + this exact sequence in `proof_offline`); [installHelper] is the one seam to repoint.
|
|
35
|
+
@internal
|
|
36
|
+
library;
|
|
37
|
+
|
|
38
|
+
import 'package:meta/meta.dart' show internal;
|
|
39
|
+
|
|
33
40
|
import 'dart:convert';
|
|
34
41
|
import 'dart:io';
|
|
35
42
|
|
|
36
43
|
import 'package:crypto/crypto.dart' show sha256;
|
|
44
|
+
import 'package:package_config/package_config.dart' show loadPackageConfigUri;
|
|
37
45
|
|
|
38
46
|
import 'dev_cloud.dart';
|
|
39
47
|
import 'node_runner_transport.dart';
|
|
48
|
+
import 'session_bound.dart' show DomainNotInstalledForSession;
|
|
40
49
|
import 'webview_bridge.dart' show NomosBridge, NomosSession;
|
|
41
50
|
|
|
51
|
+
/// Framework-internal classifier for a sync result that can never become converged by retrying. Generated
|
|
52
|
+
/// scenarios use it through [NomosTestHarness.converge]; it is public only inside `package:nomos_client/src`
|
|
53
|
+
/// so the false-green contract has a cheap regression test without booting a cloud.
|
|
54
|
+
String? nmsPermanentConvergenceFailure(Map<String, Object?> result) {
|
|
55
|
+
final admission = result['admission'];
|
|
56
|
+
final sessions = admission is Map && admission['sessions'] is List
|
|
57
|
+
? admission['sessions'] as List
|
|
58
|
+
: const [];
|
|
59
|
+
final rejected = sessions
|
|
60
|
+
.whereType<Map>()
|
|
61
|
+
.expand((session) =>
|
|
62
|
+
session['rejected'] is List ? session['rejected'] as List : const [])
|
|
63
|
+
.toList();
|
|
64
|
+
final deadLetters = admission is Map && admission['deadLetters'] is List
|
|
65
|
+
? admission['deadLetters'] as List
|
|
66
|
+
: const [];
|
|
67
|
+
final converged = result['converged'];
|
|
68
|
+
final replayDeadLetters =
|
|
69
|
+
converged is Map && converged['deadLettered'] is List
|
|
70
|
+
? converged['deadLettered'] as List
|
|
71
|
+
: const [];
|
|
72
|
+
final pushError = result['pushError'];
|
|
73
|
+
final refused = pushError is Map && pushError['refused'] == true;
|
|
74
|
+
if (rejected.isEmpty &&
|
|
75
|
+
deadLetters.isEmpty &&
|
|
76
|
+
replayDeadLetters.isEmpty &&
|
|
77
|
+
!refused) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return jsonEncode(result);
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
/// The offline-install lane configuration a harness keeps so the CUSTODY DIAGNOSTIC helpers
|
|
43
84
|
/// (ask 9 of architecture/session_bound_clients.md) can re-run installs after construction:
|
|
44
85
|
/// [seedWorkspaceFromDeployJson] / [resetWorkspaceCustody].
|
|
@@ -184,17 +225,16 @@ class NomosTestHarness {
|
|
|
184
225
|
/// with [NomosTestCloud.setOnline]; assert remote custody with [NomosTestCloud.remoteHead].
|
|
185
226
|
final NomosTestCloud? testCloud;
|
|
186
227
|
|
|
187
|
-
/// The
|
|
188
|
-
/// the
|
|
228
|
+
/// The primary application workspace. For [cloudBound] this is the identity home; for [ephemeral] it is
|
|
229
|
+
/// the one local application workspace. Internal restart logic uses it to reopen custody first.
|
|
189
230
|
final String? homeWorkspace;
|
|
190
231
|
|
|
191
|
-
/// The mounted
|
|
192
|
-
/// `HomeClient` to it). Throws for an offline harness (no home ceremony ran).
|
|
232
|
+
/// The primary mounted application session.
|
|
193
233
|
NomosSession get homeSession {
|
|
194
234
|
final ws = homeWorkspace;
|
|
195
235
|
if (ws == null) {
|
|
196
236
|
throw NomosTestHarnessException(
|
|
197
|
-
'
|
|
237
|
+
'this harness has no primary application session');
|
|
198
238
|
}
|
|
199
239
|
return session(ws);
|
|
200
240
|
}
|
|
@@ -266,6 +306,64 @@ class NomosTestHarness {
|
|
|
266
306
|
/// `bridge.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
|
|
267
307
|
String? deviceSecret(String workspace) => _deviceSecrets[workspace];
|
|
268
308
|
|
|
309
|
+
/// Framework-private scenario setup for a business intent guarded by a delegated role.
|
|
310
|
+
/// Generated `scenario.given(person).mayX()` methods are the only caller: they name the
|
|
311
|
+
/// business permission while this harness mints and admits the real parent-rooted delegation.
|
|
312
|
+
/// Tenant tests never handle relation names, certificate bytes, signer secrets, or raw offers.
|
|
313
|
+
Future<void> nomosInternalAllowDelegatedScenarioAction({
|
|
314
|
+
required String domain,
|
|
315
|
+
required String acceptanceIntent,
|
|
316
|
+
required String relation,
|
|
317
|
+
required String subject,
|
|
318
|
+
required String subjectField,
|
|
319
|
+
required String certificateField,
|
|
320
|
+
Map<String, Object?> facts = const {},
|
|
321
|
+
}) async {
|
|
322
|
+
final secret = _deviceSecrets[workspace];
|
|
323
|
+
if (secret == null || secret.isEmpty) {
|
|
324
|
+
throw NomosTestHarnessException(
|
|
325
|
+
'the generated scenario cannot establish its business permission because the test device secret is unavailable');
|
|
326
|
+
}
|
|
327
|
+
final rows = await session(workspace)
|
|
328
|
+
.queryById('birth-attestation:self', null, subject);
|
|
329
|
+
if (rows.length != 1) {
|
|
330
|
+
throw NomosTestHarnessException(
|
|
331
|
+
'the generated scenario expected one verified birth attestation before establishing business permission; found ${rows.length}');
|
|
332
|
+
}
|
|
333
|
+
final row = rows.single;
|
|
334
|
+
final data = row['data'] is Map
|
|
335
|
+
? (row['data'] as Map).cast<String, Object?>()
|
|
336
|
+
: row.cast<String, Object?>();
|
|
337
|
+
final rawChain = data['certChain'];
|
|
338
|
+
final decodedChain = rawChain is String ? jsonDecode(rawChain) : rawChain;
|
|
339
|
+
final links = decodedChain is Map && decodedChain['links'] is List
|
|
340
|
+
? List<Object?>.from(decodedChain['links'] as List)
|
|
341
|
+
: const <Object?>[];
|
|
342
|
+
final issued = await session(workspace).issueDelegationCert(
|
|
343
|
+
signerSecret: secret,
|
|
344
|
+
grantorId: (data['parentId'] ?? workspace).toString(),
|
|
345
|
+
relation: relation,
|
|
346
|
+
subject: subject,
|
|
347
|
+
certChain: links,
|
|
348
|
+
);
|
|
349
|
+
final certificate = issued['certString']?.toString();
|
|
350
|
+
if (certificate == null || certificate.isEmpty) {
|
|
351
|
+
throw NomosTestHarnessException(
|
|
352
|
+
'the generated scenario could not mint the parent-rooted business delegation');
|
|
353
|
+
}
|
|
354
|
+
await session(workspace).offerOutcome(
|
|
355
|
+
domain: domain,
|
|
356
|
+
directiveId: acceptanceIntent,
|
|
357
|
+
actor: subject,
|
|
358
|
+
authorSecret: secret,
|
|
359
|
+
payload: <String, Object?>{
|
|
360
|
+
...facts,
|
|
361
|
+
subjectField: subject,
|
|
362
|
+
certificateField: certificate,
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
269
367
|
/// The workspace names on this device's local plane (one for [ephemeral], N for [cluster]).
|
|
270
368
|
List<String> get sessions => List.unmodifiable(_sessionNames);
|
|
271
369
|
|
|
@@ -299,6 +397,7 @@ class NomosTestHarness {
|
|
|
299
397
|
/// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
|
|
300
398
|
static Future<NomosTestHarness> ephemeral({
|
|
301
399
|
required List<String> deployJsonPaths,
|
|
400
|
+
String? ownerSubject,
|
|
302
401
|
String workspace = 'test-ws',
|
|
303
402
|
String clientId = 'test-runner',
|
|
304
403
|
String? runtimeDir,
|
|
@@ -320,6 +419,9 @@ class NomosTestHarness {
|
|
|
320
419
|
}
|
|
321
420
|
final entry = nodeEntry ??
|
|
322
421
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
422
|
+
final effectiveRuntimeDir = runtimeDir ??
|
|
423
|
+
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
424
|
+
Platform.environment['NOMOS_OFFLINE_RUNTIME'];
|
|
323
425
|
// THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
|
|
324
426
|
// `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
|
|
325
427
|
// bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
|
|
@@ -337,6 +439,18 @@ class NomosTestHarness {
|
|
|
337
439
|
File(resolvedHelper).existsSync();
|
|
338
440
|
final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
|
|
339
441
|
|
|
442
|
+
final owner = ownerSubject?.trim();
|
|
443
|
+
final birthsApplicationHome = owner != null &&
|
|
444
|
+
owner.isNotEmpty &&
|
|
445
|
+
workspace == _receivingGroupNameOf(owner);
|
|
446
|
+
// A receiving-group address is not an arbitrary law-install target. It is valid only when a parent
|
|
447
|
+
// births it with the attestation, signer seating and warrant ordered into genesis. Generated scenarios
|
|
448
|
+
// therefore install their package on a private throwaway application parent and exercise the same local
|
|
449
|
+
// birth machinery production uses. Raw/law-focused harness callers keep their explicitly named workspace.
|
|
450
|
+
final installWorkspace = birthsApplicationHome
|
|
451
|
+
? 'nms-app-${DateTime.now().microsecondsSinceEpoch.toRadixString(36)}'
|
|
452
|
+
: workspace;
|
|
453
|
+
|
|
340
454
|
// A FRESH temp dir per harness — the snapshot + any transient runner state live here and nowhere else,
|
|
341
455
|
// so dispose() leaves nothing behind.
|
|
342
456
|
final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
|
|
@@ -350,7 +464,7 @@ class NomosTestHarness {
|
|
|
350
464
|
final info = await _phased(
|
|
351
465
|
phase:
|
|
352
466
|
'1/4: offline law install (${deployJsonPaths.length} package(s))',
|
|
353
|
-
workspace:
|
|
467
|
+
workspace: installWorkspace,
|
|
354
468
|
awaiting: useInRepoScript
|
|
355
469
|
? 'offline-install.ts'
|
|
356
470
|
: 'githolon harness-install',
|
|
@@ -366,11 +480,22 @@ class NomosTestHarness {
|
|
|
366
480
|
inRepoScript: resolvedHelper,
|
|
367
481
|
githolonCmd: githolonCmd,
|
|
368
482
|
deployJsonPaths: deployJsonPaths,
|
|
369
|
-
workspace:
|
|
483
|
+
workspace: installWorkspace,
|
|
370
484
|
snapshot: snapshot,
|
|
371
|
-
runtimeDir:
|
|
485
|
+
runtimeDir: effectiveRuntimeDir,
|
|
372
486
|
onDiagnostic: onDiagnostic));
|
|
373
487
|
|
|
488
|
+
final runnerEnvironment = <String, String>{
|
|
489
|
+
'NOMOS_OFFLINE': '1',
|
|
490
|
+
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
491
|
+
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
492
|
+
if (info['devicePublic'] != null)
|
|
493
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
|
|
494
|
+
if (info['deviceSecret'] != null)
|
|
495
|
+
'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
|
|
496
|
+
if (effectiveRuntimeDir != null)
|
|
497
|
+
'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
|
|
498
|
+
};
|
|
374
499
|
transport = await _phased(
|
|
375
500
|
phase: '2/4: spawn the node runner',
|
|
376
501
|
awaiting: 'Process.start($node)',
|
|
@@ -380,16 +505,7 @@ class NomosTestHarness {
|
|
|
380
505
|
entry: entry,
|
|
381
506
|
resolveFrom: resolveFrom,
|
|
382
507
|
node: node,
|
|
383
|
-
environment:
|
|
384
|
-
'NOMOS_OFFLINE': '1',
|
|
385
|
-
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
386
|
-
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
387
|
-
if (info['devicePublic'] != null)
|
|
388
|
-
'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
|
|
389
|
-
if (info['deviceSecret'] != null)
|
|
390
|
-
'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
|
|
391
|
-
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
392
|
-
},
|
|
508
|
+
environment: runnerEnvironment,
|
|
393
509
|
onDiagnostic: onDiagnostic,
|
|
394
510
|
));
|
|
395
511
|
final plane = NomosBridge(transport!);
|
|
@@ -405,7 +521,7 @@ class NomosTestHarness {
|
|
|
405
521
|
run: () => plane.ready);
|
|
406
522
|
await _phased(
|
|
407
523
|
phase: '4/4: connect session',
|
|
408
|
-
workspace:
|
|
524
|
+
workspace: installWorkspace,
|
|
409
525
|
awaiting: 'connect',
|
|
410
526
|
timeout: phaseTimeout ?? connectTimeout,
|
|
411
527
|
onDiagnostic: onDiagnostic,
|
|
@@ -416,7 +532,8 @@ class NomosTestHarness {
|
|
|
416
532
|
run: () => plane.connect(
|
|
417
533
|
cloud:
|
|
418
534
|
'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
419
|
-
workspace:
|
|
535
|
+
workspace: installWorkspace,
|
|
536
|
+
session: installWorkspace,
|
|
420
537
|
clientId: clientId,
|
|
421
538
|
allowUnborn: true,
|
|
422
539
|
// THE HARNESS DEVICE (era-0): the install lane mints one device key per plane — sessions
|
|
@@ -424,6 +541,197 @@ class NomosTestHarness {
|
|
|
424
541
|
// with the SAME key, so offline `.births()` offers Just Work.
|
|
425
542
|
authorSecret: info['deviceSecret']?.toString(),
|
|
426
543
|
));
|
|
544
|
+
if (birthsApplicationHome) {
|
|
545
|
+
final devicePublic = info['devicePublic']?.toString();
|
|
546
|
+
final deviceSecret = info['deviceSecret']?.toString();
|
|
547
|
+
final framework = info['frameworkHash']?.toString();
|
|
548
|
+
final laws = ((info['lawHashes'] as List?) ?? const [])
|
|
549
|
+
.map((value) => value.toString())
|
|
550
|
+
.where((value) => value.isNotEmpty)
|
|
551
|
+
.toList();
|
|
552
|
+
if (devicePublic == null ||
|
|
553
|
+
devicePublic.isEmpty ||
|
|
554
|
+
deviceSecret == null ||
|
|
555
|
+
deviceSecret.isEmpty ||
|
|
556
|
+
framework == null ||
|
|
557
|
+
framework.isEmpty ||
|
|
558
|
+
laws.isEmpty) {
|
|
559
|
+
throw NomosTestHarnessException(
|
|
560
|
+
'the offline application parent did not return the framework law, application law and test device needed to birth the signed-in application');
|
|
561
|
+
}
|
|
562
|
+
final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
|
|
563
|
+
final outcome = await _phased(
|
|
564
|
+
phase: 'application: birth the signed-in application',
|
|
565
|
+
workspace: workspace,
|
|
566
|
+
awaiting: 'attested receiving-group birth',
|
|
567
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
568
|
+
onDiagnostic: onDiagnostic,
|
|
569
|
+
run: () => plane.session(installWorkspace).offerOutcome(
|
|
570
|
+
domain: '__nomos_application_home_v1',
|
|
571
|
+
directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
|
|
572
|
+
actor: owner,
|
|
573
|
+
authorSecret: deviceSecret,
|
|
574
|
+
payload: <String, Object?>{
|
|
575
|
+
'owner': owner,
|
|
576
|
+
'ownerKeyHash': keyHash,
|
|
577
|
+
'frameworkHash': framework,
|
|
578
|
+
'lawHash': laws.first,
|
|
579
|
+
'bornAt': DateTime.now().toUtc().toIso8601String(),
|
|
580
|
+
},
|
|
581
|
+
),
|
|
582
|
+
);
|
|
583
|
+
String? bornStatus;
|
|
584
|
+
for (final entry in outcome.birthOutcomes) {
|
|
585
|
+
if (entry['workspace']?.toString() == workspace) {
|
|
586
|
+
bornStatus = entry['status']?.toString();
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (outcome.born.length != 1 ||
|
|
591
|
+
outcome.born.single != workspace ||
|
|
592
|
+
bornStatus != 'born') {
|
|
593
|
+
throw NomosTestHarnessException(
|
|
594
|
+
'the offline application parent did not birth "$workspace" cleanly '
|
|
595
|
+
'(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})');
|
|
596
|
+
}
|
|
597
|
+
await _phased(
|
|
598
|
+
phase: 'application: mount the signed-in application',
|
|
599
|
+
workspace: workspace,
|
|
600
|
+
awaiting: 'connect born receiving group',
|
|
601
|
+
timeout: phaseTimeout ?? connectTimeout,
|
|
602
|
+
onDiagnostic: onDiagnostic,
|
|
603
|
+
run: () => plane.connect(
|
|
604
|
+
cloud: 'https://offline.invalid',
|
|
605
|
+
workspace: workspace,
|
|
606
|
+
session: workspace,
|
|
607
|
+
clientId: '$clientId-home',
|
|
608
|
+
authorSecret: deviceSecret,
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
} else if (owner != null && owner.isNotEmpty) {
|
|
612
|
+
final devicePublic = info['devicePublic']?.toString();
|
|
613
|
+
final deviceSecret = info['deviceSecret']?.toString();
|
|
614
|
+
if (devicePublic == null ||
|
|
615
|
+
devicePublic.isEmpty ||
|
|
616
|
+
deviceSecret == null ||
|
|
617
|
+
deviceSecret.isEmpty) {
|
|
618
|
+
throw NomosTestHarnessException(
|
|
619
|
+
'the offline install did not return the test device key needed to make "$owner" an application-ready signer');
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
|
|
623
|
+
await _phased(
|
|
624
|
+
phase: 'owner: seat the test device signer',
|
|
625
|
+
workspace: workspace,
|
|
626
|
+
awaiting: 'verified test-root signer enrollment',
|
|
627
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
628
|
+
onDiagnostic: onDiagnostic,
|
|
629
|
+
// A generated TestPerson stands in for a real signed-in person. Seat the throwaway
|
|
630
|
+
// harness device through the installed framework law with the same verified provenance
|
|
631
|
+
// that the private test root later attests. Production clients never manufacture this
|
|
632
|
+
// marker; it exists only inside the offline test-mint ceremony.
|
|
633
|
+
run: () => plane.session(workspace).offerOutcome(
|
|
634
|
+
domain: '__nomos_application_home_v1',
|
|
635
|
+
directiveId: 'enrollSigner',
|
|
636
|
+
actor: owner,
|
|
637
|
+
authorSecret: deviceSecret,
|
|
638
|
+
payload: <String, Object?>{
|
|
639
|
+
'keyHash': keyHash,
|
|
640
|
+
'grantedBy': owner,
|
|
641
|
+
'grantedAt': 1,
|
|
642
|
+
'verifiedVia': 'nomos:test-mint',
|
|
643
|
+
},
|
|
644
|
+
),
|
|
645
|
+
);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
// Pre-application-home packages neither expose nor enforce this
|
|
648
|
+
// signer route. Preserve their old harness behaviour, while keeping
|
|
649
|
+
// every refusal from a current package fatal.
|
|
650
|
+
if (!error.toString().contains(
|
|
651
|
+
"no active installed law declares domain '__nomos_application_home_v1'")) {
|
|
652
|
+
rethrow;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
final bornAt = DateTime.now().toUtc().toIso8601String();
|
|
657
|
+
await _phased(
|
|
658
|
+
phase: 'owner: make the test application ready',
|
|
659
|
+
workspace: workspace,
|
|
660
|
+
awaiting: 'framework owner birth',
|
|
661
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
662
|
+
onDiagnostic: onDiagnostic,
|
|
663
|
+
run: () async {
|
|
664
|
+
await plane.session(workspace).offerOutcome(
|
|
665
|
+
domain: '__nomos_application_home_v1',
|
|
666
|
+
directiveId: 'nomosInternalSeedReceivingGroupPolicy7c1f5e',
|
|
667
|
+
actor: 'host-birth',
|
|
668
|
+
payload: <String, Object?>{'setAt': bornAt},
|
|
669
|
+
);
|
|
670
|
+
await plane.session(workspace).offerOutcome(
|
|
671
|
+
domain: '__nomos_application_home_v1',
|
|
672
|
+
directiveId: 'nomosInternalSeedReceivingGroup7c1f5e',
|
|
673
|
+
actor: 'host-birth',
|
|
674
|
+
payload: <String, Object?>{
|
|
675
|
+
'principal': owner,
|
|
676
|
+
'workspaceName': _receivingGroupNameOf(owner),
|
|
677
|
+
'bornAt': bornAt,
|
|
678
|
+
},
|
|
679
|
+
);
|
|
680
|
+
},
|
|
681
|
+
);
|
|
682
|
+
final parentCert = await plane.signBirthCert(<String, Object?>{
|
|
683
|
+
'child': workspace,
|
|
684
|
+
'founder': owner,
|
|
685
|
+
'verified': true,
|
|
686
|
+
'parentKey': devicePublic,
|
|
687
|
+
'parentId': 'root',
|
|
688
|
+
'epoch': 0,
|
|
689
|
+
}, deviceSecret);
|
|
690
|
+
await _phased(
|
|
691
|
+
phase: 'owner: establish the verified test application root',
|
|
692
|
+
workspace: workspace,
|
|
693
|
+
awaiting: 'birth attestation then genesis warrant',
|
|
694
|
+
timeout: phaseTimeout ?? _kOtherDeadline,
|
|
695
|
+
onDiagnostic: onDiagnostic,
|
|
696
|
+
run: () async {
|
|
697
|
+
await plane.session(workspace).offerOutcome(
|
|
698
|
+
domain: '__nomos_application_home_v1',
|
|
699
|
+
directiveId: 'seedBirthAttestation',
|
|
700
|
+
actor: owner,
|
|
701
|
+
authorSecret: deviceSecret,
|
|
702
|
+
payload: <String, Object?>{
|
|
703
|
+
'child': workspace,
|
|
704
|
+
'founder': owner,
|
|
705
|
+
'verified': 'true',
|
|
706
|
+
'parentId': 'root',
|
|
707
|
+
'parentKey': devicePublic,
|
|
708
|
+
'epoch': 0,
|
|
709
|
+
'certChain': jsonEncode(<String, Object?>{
|
|
710
|
+
'links': <Object?>[parentCert],
|
|
711
|
+
}),
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
await plane.session(workspace).offerOutcome(
|
|
715
|
+
domain: '__nomos_application_home_v1',
|
|
716
|
+
directiveId: 'seedGenesis',
|
|
717
|
+
actor: owner,
|
|
718
|
+
authorSecret: deviceSecret,
|
|
719
|
+
payload: <String, Object?>{
|
|
720
|
+
'principal': owner,
|
|
721
|
+
'workspaceId': workspace,
|
|
722
|
+
'bornAt': '2000-01-01T00:00:00.000Z',
|
|
723
|
+
},
|
|
724
|
+
);
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
} on DomainNotInstalledForSession {
|
|
728
|
+
// Applications that do not declare the compiler-owned home domain do
|
|
729
|
+
// not have an owner seat to prepare. Only that precise absence is
|
|
730
|
+
// optional: a refusal from an installed home law must still fail the
|
|
731
|
+
// scenario, because hiding it would make an invalid fixture appear
|
|
732
|
+
// application-ready.
|
|
733
|
+
}
|
|
734
|
+
}
|
|
427
735
|
phase(
|
|
428
736
|
'ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
|
|
429
737
|
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
@@ -433,7 +741,7 @@ class NomosTestHarness {
|
|
|
433
741
|
// dev's first test. [bridge] is a SESSION HANDLE pinned to [workspace], so tenant code written
|
|
434
742
|
// against the harness keeps working unchanged; only code that grabs the raw plane and assumes
|
|
435
743
|
// single-session trips. Opt out with `decoySession: false` (please don't).
|
|
436
|
-
if (decoySession) {
|
|
744
|
+
if (decoySession && !birthsApplicationHome) {
|
|
437
745
|
await _phased(
|
|
438
746
|
phase: 'decoy: connect the decoy session',
|
|
439
747
|
workspace: _decoyWorkspace,
|
|
@@ -473,9 +781,20 @@ class NomosTestHarness {
|
|
|
473
781
|
useInRepoScript: useInRepoScript,
|
|
474
782
|
inRepoScript: resolvedHelper,
|
|
475
783
|
githolonCmd: githolonCmd,
|
|
476
|
-
runtimeDir:
|
|
784
|
+
runtimeDir: effectiveRuntimeDir,
|
|
477
785
|
clientId: clientId),
|
|
478
|
-
phaseTimeout: phaseTimeout
|
|
786
|
+
phaseTimeout: phaseTimeout,
|
|
787
|
+
cloudRestartLane: _CloudRestartLane(
|
|
788
|
+
entry: entry,
|
|
789
|
+
node: node,
|
|
790
|
+
cloud: 'https://offline.invalid',
|
|
791
|
+
authToken: '',
|
|
792
|
+
authorSecret: info['deviceSecret']?.toString() ?? '',
|
|
793
|
+
clientId: clientId,
|
|
794
|
+
resolveFrom: resolveFrom,
|
|
795
|
+
environment: runnerEnvironment,
|
|
796
|
+
),
|
|
797
|
+
homeWorkspace: workspace);
|
|
479
798
|
if (autoTearDown && registerTearDown != null) {
|
|
480
799
|
registerTearDown(harness.dispose);
|
|
481
800
|
}
|
|
@@ -535,6 +854,9 @@ class NomosTestHarness {
|
|
|
535
854
|
}
|
|
536
855
|
final entry = nodeEntry ??
|
|
537
856
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
857
|
+
final effectiveRuntimeDir = runtimeDir ??
|
|
858
|
+
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
859
|
+
Platform.environment['NOMOS_OFFLINE_RUNTIME'];
|
|
538
860
|
final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
|
|
539
861
|
final resolvedHelper = installHelper ??
|
|
540
862
|
(File(adjacentInstaller).existsSync()
|
|
@@ -579,7 +901,7 @@ class NomosTestHarness {
|
|
|
579
901
|
deployJsonPaths: e.value,
|
|
580
902
|
workspace: e.key,
|
|
581
903
|
snapshot: snap,
|
|
582
|
-
runtimeDir:
|
|
904
|
+
runtimeDir: effectiveRuntimeDir,
|
|
583
905
|
onDiagnostic: onDiagnostic));
|
|
584
906
|
snapshots[e.key] = snap;
|
|
585
907
|
lawHashesByWs[e.key] = ((info['lawHashes'] as List?) ?? const [])
|
|
@@ -616,7 +938,8 @@ class NomosTestHarness {
|
|
|
616
938
|
'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
|
|
617
939
|
if (planeTestRootSecret != null)
|
|
618
940
|
'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
|
|
619
|
-
if (
|
|
941
|
+
if (effectiveRuntimeDir != null)
|
|
942
|
+
'NOMOS_OFFLINE_RUNTIME': effectiveRuntimeDir,
|
|
620
943
|
},
|
|
621
944
|
onDiagnostic: onDiagnostic,
|
|
622
945
|
));
|
|
@@ -721,7 +1044,7 @@ class NomosTestHarness {
|
|
|
721
1044
|
useInRepoScript: useInRepoScript,
|
|
722
1045
|
inRepoScript: resolvedHelper,
|
|
723
1046
|
githolonCmd: githolonCmd,
|
|
724
|
-
runtimeDir:
|
|
1047
|
+
runtimeDir: effectiveRuntimeDir,
|
|
725
1048
|
clientId: clientId),
|
|
726
1049
|
phaseTimeout: phaseTimeout);
|
|
727
1050
|
if (autoTearDown && registerTearDown != null) {
|
|
@@ -881,15 +1204,16 @@ class NomosTestHarness {
|
|
|
881
1204
|
'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)');
|
|
882
1205
|
}
|
|
883
1206
|
final birthRoutes = frameworkPostRoutes
|
|
884
|
-
.where((route) =>
|
|
1207
|
+
.where((route) =>
|
|
1208
|
+
route.directive == 'nomosInternalBirthReceivingGroup7c1f5e')
|
|
885
1209
|
.toSet()
|
|
886
1210
|
.toList();
|
|
887
1211
|
if (birthRoutes.length != 1) {
|
|
888
1212
|
throw NomosTestHarnessException(
|
|
889
|
-
'cloudBound: the deployed package must expose exactly one
|
|
1213
|
+
'cloudBound: the deployed package must expose exactly one framework receiving-group route; found ${birthRoutes.length}');
|
|
890
1214
|
}
|
|
891
1215
|
final applicationHomeDomain = birthRoutes.single.domain;
|
|
892
|
-
final
|
|
1216
|
+
final birthReceivingGroupPath = birthRoutes.single.path;
|
|
893
1217
|
final enrollmentRoutes = frameworkPostRoutes
|
|
894
1218
|
.where((route) =>
|
|
895
1219
|
route.directive == 'enrollFirstDevice' &&
|
|
@@ -1008,7 +1332,7 @@ class NomosTestHarness {
|
|
|
1008
1332
|
final delegationCert = await plane.mintKeylessDelegation({
|
|
1009
1333
|
'user': subject,
|
|
1010
1334
|
'deviceKey': publicB64,
|
|
1011
|
-
'childName':
|
|
1335
|
+
'childName': _receivingGroupNameOf(subject),
|
|
1012
1336
|
'parentId': platformWorkspace,
|
|
1013
1337
|
'verifiedVia': issuer,
|
|
1014
1338
|
'idpToken': idpToken,
|
|
@@ -1044,11 +1368,12 @@ class NomosTestHarness {
|
|
|
1044
1368
|
await _phased(
|
|
1045
1369
|
phase: '5/6: remote home-birth ceremony (attested)',
|
|
1046
1370
|
workspace: platformWorkspace,
|
|
1047
|
-
awaiting:
|
|
1371
|
+
awaiting:
|
|
1372
|
+
'POST /v2/workspaces/$platformWorkspace$birthReceivingGroupPath',
|
|
1048
1373
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
1049
1374
|
onDiagnostic: onDiagnostic,
|
|
1050
1375
|
run: () async {
|
|
1051
|
-
final homeName =
|
|
1376
|
+
final homeName = _receivingGroupNameOf(subject);
|
|
1052
1377
|
final homeCert = await plane.signBirthCert({
|
|
1053
1378
|
'child': homeName,
|
|
1054
1379
|
'founder': subject,
|
|
@@ -1059,8 +1384,8 @@ class NomosTestHarness {
|
|
|
1059
1384
|
'parentId': platformWorkspace,
|
|
1060
1385
|
'epoch': 0,
|
|
1061
1386
|
}, device.secret);
|
|
1062
|
-
final out =
|
|
1063
|
-
|
|
1387
|
+
final out = await post(
|
|
1388
|
+
'/v2/workspaces/$platformWorkspace$birthReceivingGroupPath', {
|
|
1064
1389
|
'owner': subject,
|
|
1065
1390
|
'ownerKeyHash': device.keyHash,
|
|
1066
1391
|
'frameworkHash': frameworkHash,
|
|
@@ -1069,9 +1394,10 @@ class NomosTestHarness {
|
|
|
1069
1394
|
'homeCert': jsonEncode(homeCert),
|
|
1070
1395
|
'actor': subject,
|
|
1071
1396
|
'authorSecret': device.secret,
|
|
1072
|
-
},
|
|
1073
|
-
|
|
1074
|
-
|
|
1397
|
+
},
|
|
1398
|
+
headers: {
|
|
1399
|
+
'x-nomos-auth': device.idpToken
|
|
1400
|
+
});
|
|
1075
1401
|
final born = (out['born'] as List?) ?? const [];
|
|
1076
1402
|
final bornNames = born
|
|
1077
1403
|
.map((b) => (b as Map)['workspace']?.toString())
|
|
@@ -1154,10 +1480,9 @@ class NomosTestHarness {
|
|
|
1154
1480
|
}
|
|
1155
1481
|
}
|
|
1156
1482
|
|
|
1157
|
-
///
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
'home-${sha256.convert(utf8.encode(subject)).toString().substring(0, 40)}';
|
|
1483
|
+
/// Compiler/runtime-only receiving-group address. The law independently re-derives the same value.
|
|
1484
|
+
static String _receivingGroupNameOf(String principal) =>
|
|
1485
|
+
'nomos-rg-${sha256.convert(utf8.encode(principal)).toString().substring(0, 40)}';
|
|
1161
1486
|
|
|
1162
1487
|
/// CONVERGE local custody with the test cloud (co2 topology step 8): sync every connected session —
|
|
1163
1488
|
/// PARENT-FIRST by connect order, so the home's chain lands remotely and its birth offer-effects create
|
|
@@ -1184,8 +1509,18 @@ class NomosTestHarness {
|
|
|
1184
1509
|
final r = (await bridge.session(ws).sync(admit: true))
|
|
1185
1510
|
.cast<String, Object?>();
|
|
1186
1511
|
last[ws] = r;
|
|
1187
|
-
|
|
1188
|
-
|
|
1512
|
+
final permanentFailure = nmsPermanentConvergenceFailure(r);
|
|
1513
|
+
if (permanentFailure != null) {
|
|
1514
|
+
throw NomosTestHarnessException(
|
|
1515
|
+
'converge: $ws rejected or dead-lettered business work — $permanentFailure',
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
if (r['pushFailed'] != true &&
|
|
1519
|
+
r['stillAhead'] != true &&
|
|
1520
|
+
r['offline'] != true &&
|
|
1521
|
+
r['deferred'] != true) pending.remove(ws);
|
|
1522
|
+
} on NomosTestHarnessException {
|
|
1523
|
+
rethrow;
|
|
1189
1524
|
} catch (e) {
|
|
1190
1525
|
last[ws] = {'error': e.toString()};
|
|
1191
1526
|
}
|
|
@@ -1403,7 +1738,7 @@ class NomosTestHarness {
|
|
|
1403
1738
|
final home = homeWorkspace;
|
|
1404
1739
|
if (lane == null || home == null) {
|
|
1405
1740
|
throw NomosTestHarnessException(
|
|
1406
|
-
'restart()
|
|
1741
|
+
'restart() requires a harness with one primary application session');
|
|
1407
1742
|
}
|
|
1408
1743
|
|
|
1409
1744
|
final oldPlane = _plane;
|
|
@@ -1490,9 +1825,63 @@ class NomosTestHarness {
|
|
|
1490
1825
|
}
|
|
1491
1826
|
}
|
|
1492
1827
|
|
|
1493
|
-
///
|
|
1494
|
-
///
|
|
1495
|
-
///
|
|
1828
|
+
/// Simulate reinstalling the application with no local ledger custody. Every application workspace must
|
|
1829
|
+
/// reopen from remote custody; a successful business read afterward is therefore a replay proof, not a
|
|
1830
|
+
/// read from the old process snapshot.
|
|
1831
|
+
Future<void> reinstall() async {
|
|
1832
|
+
if (_disposed) {
|
|
1833
|
+
throw NomosTestHarnessException(
|
|
1834
|
+
'reinstall(): this test device has already been disposed');
|
|
1835
|
+
}
|
|
1836
|
+
final lane = _cloudRestartLane;
|
|
1837
|
+
final home = homeWorkspace;
|
|
1838
|
+
if (lane == null || home == null || testCloud == null) {
|
|
1839
|
+
throw NomosTestHarnessException(
|
|
1840
|
+
'reinstall() requires a connected scenario with remote custody');
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
final oldPlane = _plane;
|
|
1844
|
+
final targets = oldPlane.connectedSessions
|
|
1845
|
+
.where(
|
|
1846
|
+
(ws) => ws != _decoyWorkspace && !ws.endsWith('-onboard-scratch'))
|
|
1847
|
+
.toList();
|
|
1848
|
+
if (targets.remove(home)) targets.insert(0, home);
|
|
1849
|
+
for (final ws in targets) {
|
|
1850
|
+
await oldPlane.session(ws).stopAutoSync();
|
|
1851
|
+
}
|
|
1852
|
+
await _transport.dispose();
|
|
1853
|
+
|
|
1854
|
+
_transport = await NodeRunnerTransport.start(
|
|
1855
|
+
entry: lane.entry,
|
|
1856
|
+
resolveFrom: lane.resolveFrom,
|
|
1857
|
+
node: lane.node,
|
|
1858
|
+
environment: lane.environment,
|
|
1859
|
+
onDiagnostic: _onDiagnostic,
|
|
1860
|
+
);
|
|
1861
|
+
final reinstalled = NomosBridge(_transport);
|
|
1862
|
+
await reinstalled.ready;
|
|
1863
|
+
for (final ws in targets) {
|
|
1864
|
+
await reinstalled.connect(
|
|
1865
|
+
cloud: lane.cloud,
|
|
1866
|
+
workspace: ws,
|
|
1867
|
+
session: ws,
|
|
1868
|
+
clientId: '${lane.clientId}-reinstall-$ws',
|
|
1869
|
+
authToken: lane.authToken,
|
|
1870
|
+
authorSecret: lane.authorSecret,
|
|
1871
|
+
);
|
|
1872
|
+
}
|
|
1873
|
+
bridge = reinstalled.session(home);
|
|
1874
|
+
_sessionNames
|
|
1875
|
+
..clear()
|
|
1876
|
+
..addAll(targets);
|
|
1877
|
+
for (final ws in targets) {
|
|
1878
|
+
await reinstalled.session(ws).autoSync(fallbackMs: 60000);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
/// Deterministic, idempotent, total-ordered teardown: close the bridge and all live watches, stop the runner
|
|
1883
|
+
/// (SIGTERM → 5s deadline → SIGKILL, via [NodeRunnerTransport.dispose]), then delete the temp dir. Safe to
|
|
1884
|
+
/// call twice; safe to call after a failed setup. Nothing left running, nothing left on disk.
|
|
1496
1885
|
/// Disposal must ALWAYS complete (never leave a hung teardown holding the test runner): the transport
|
|
1497
1886
|
/// dispose is deadlined too, and a breach is LOGGED (not thrown — throwing from teardown would mask the
|
|
1498
1887
|
/// test's own outcome) before the temp dir is still deleted.
|
|
@@ -1502,6 +1891,20 @@ class NomosTestHarness {
|
|
|
1502
1891
|
final f = _onDiagnostic;
|
|
1503
1892
|
if (f != null)
|
|
1504
1893
|
f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
|
|
1894
|
+
try {
|
|
1895
|
+
// The bridge owns the transport's one message subscription and every watch controller. Closing the
|
|
1896
|
+
// transport first makes its incoming stream wait on consumers that can no longer receive their
|
|
1897
|
+
// unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
|
|
1898
|
+
await _plane.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
|
|
1899
|
+
onTimeout: () => throw HarnessPhaseTimeout(
|
|
1900
|
+
phase: 'dispose: close bridge watches',
|
|
1901
|
+
workspace: workspace,
|
|
1902
|
+
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
1903
|
+
awaiting: 'NomosBridge.dispose'));
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
_diag(
|
|
1906
|
+
'[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e');
|
|
1907
|
+
}
|
|
1505
1908
|
try {
|
|
1506
1909
|
await _transport.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
|
|
1507
1910
|
onTimeout: () => throw HarnessPhaseTimeout(
|
|
@@ -1583,7 +1986,7 @@ class NomosTestHarness {
|
|
|
1583
1986
|
}
|
|
1584
1987
|
if (r.exitCode != 0) {
|
|
1585
1988
|
throw NomosTestHarnessException(
|
|
1586
|
-
'offline law install failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}');
|
|
1989
|
+
'offline law install for "$workspace" failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}');
|
|
1587
1990
|
}
|
|
1588
1991
|
// The helper prints one JSON line on success; verify it before spawning the runner.
|
|
1589
1992
|
final out = (r.stdout as String).trim();
|
|
@@ -1603,6 +2006,34 @@ class NomosTestHarness {
|
|
|
1603
2006
|
}
|
|
1604
2007
|
|
|
1605
2008
|
static Future<String> _resolveEntry(String node, String from) async {
|
|
2009
|
+
String? packageConfigPath = Platform.packageConfig;
|
|
2010
|
+
if (packageConfigPath == null) {
|
|
2011
|
+
var directory = Directory(from).absolute;
|
|
2012
|
+
while (true) {
|
|
2013
|
+
final candidate = File(
|
|
2014
|
+
'${directory.path}${Platform.pathSeparator}.dart_tool${Platform.pathSeparator}package_config.json',
|
|
2015
|
+
);
|
|
2016
|
+
if (candidate.existsSync()) {
|
|
2017
|
+
packageConfigPath = candidate.uri.toString();
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
final parent = directory.parent;
|
|
2021
|
+
if (parent.path == directory.path) break;
|
|
2022
|
+
directory = parent;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
if (packageConfigPath != null) {
|
|
2026
|
+
final packages = await loadPackageConfigUri(
|
|
2027
|
+
Uri.base.resolve(packageConfigPath),
|
|
2028
|
+
);
|
|
2029
|
+
final bundled = packages.resolve(
|
|
2030
|
+
Uri.parse('package:nomos_client/src/runner/node-entry.mjs'),
|
|
2031
|
+
);
|
|
2032
|
+
if (bundled != null && bundled.scheme == 'file') {
|
|
2033
|
+
final path = bundled.toFilePath();
|
|
2034
|
+
if (File(path).existsSync()) return path;
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
1606
2037
|
// PREFER the BUILT .mjs (the published-consumer entry): node refuses type-stripping under node_modules
|
|
1607
2038
|
// (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING — co2 DEFECT P3-002), so the raw .ts entry only boots
|
|
1608
2039
|
// in-repo. The .mjs ships from @githolon/client ≥0.97.2; the .ts fallback keeps in-repo dev working.
|
|
@@ -1619,8 +2050,9 @@ class NomosTestHarness {
|
|
|
1619
2050
|
if (r.exitCode == 0 && out.isNotEmpty) return out;
|
|
1620
2051
|
}
|
|
1621
2052
|
throw NomosTestHarnessException(
|
|
1622
|
-
'
|
|
1623
|
-
'
|
|
2053
|
+
'the Nomos release contains no bundled node runner and '
|
|
2054
|
+
'@githolon/client/runner/node-entry.{mjs,ts} cannot be resolved from $from. '
|
|
2055
|
+
'Reinstall nomos_client, or pass nodeEntry: only when developing Nomos itself.');
|
|
1624
2056
|
}
|
|
1625
2057
|
}
|
|
1626
2058
|
|