@githolon/dsl 0.100.5 → 0.100.6
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 +6 -0
- package/dart/lib/src/test_harness.dart +146 -59
- package/dart/pubspec.yaml +1 -1
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +106 -87
- package/dart/test/local_sync_inert_test.dart +10 -5
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +2 -2
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/package.json +1 -1
- package/src/birth_meter_ensure.ts +157 -29
- package/src/build_package.ts +41 -6
- package/src/business_birth.ts +31 -9
- package/src/codegen_dart_client.ts +11 -7
- package/src/compile_package_main.ts +17 -8
- package/src/shape_compat.ts +14 -3
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.62.2
|
|
2
|
+
|
|
3
|
+
- Make `NomosTestHarness` own a complete, verifiable local trust lineage. Tests still bind a business actor
|
|
4
|
+
and call the generated birth method; the harness privately mints its test root, seats the signer and
|
|
5
|
+
delegation, lands the warrant in order, and refuses any child whose full chain does not replay to that root.
|
|
6
|
+
|
|
1
7
|
## 0.62.1
|
|
2
8
|
|
|
3
9
|
- Align the public Dart documentation with the one Flutter application entry point: identity comes from the
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
|
|
25
25
|
//
|
|
26
26
|
// PREREQUISITES (documented so a failure is legible): `node` on PATH; a resolvable
|
|
27
|
-
// `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
|
|
27
|
+
// `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon-testmint.wasm + packages.json —
|
|
28
28
|
// one `githolon` run fills it). All local; no network at test time.
|
|
29
29
|
//
|
|
30
30
|
// PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
|
|
@@ -173,8 +173,8 @@ class NomosTestHarness {
|
|
|
173
173
|
return session(ws);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
NomosTestHarness._(this.bridge, this.workspace, this._transport,
|
|
177
|
-
this._onDiagnostic, List<String> sessionNames,
|
|
176
|
+
NomosTestHarness._(this.bridge, this.workspace, this._transport,
|
|
177
|
+
this._workdir, this._onDiagnostic, List<String> sessionNames,
|
|
178
178
|
{Map<String, String> devicePublics = const {},
|
|
179
179
|
Map<String, String> deviceSecrets = const {},
|
|
180
180
|
this.frameworkHash,
|
|
@@ -184,7 +184,8 @@ class NomosTestHarness {
|
|
|
184
184
|
Duration? phaseTimeout,
|
|
185
185
|
this.testCloud,
|
|
186
186
|
this.homeWorkspace})
|
|
187
|
-
: _sessionNames = List.of(
|
|
187
|
+
: _sessionNames = List.of(
|
|
188
|
+
sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
|
|
188
189
|
_devicePublics = devicePublics,
|
|
189
190
|
_deviceSecrets = deviceSecrets,
|
|
190
191
|
_lawHashes = Map.of(lawHashes),
|
|
@@ -256,7 +257,8 @@ class NomosTestHarness {
|
|
|
256
257
|
/// [deployJsonPaths] — the `build/<name>.deploy.json` files `nomos-compile` emits (installed in order;
|
|
257
258
|
/// framework/platform law before a domain that depends on it).
|
|
258
259
|
/// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
|
|
259
|
-
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` +
|
|
260
|
+
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon-testmint.wasm` +
|
|
261
|
+
/// `packages.json`. The harness owns a throwaway test root; app code never handles its key material.
|
|
260
262
|
/// [nodeEntry] — path to `node-entry.ts`; when omitted it is resolved from [resolveFrom] via
|
|
261
263
|
/// `require.resolve('@githolon/client/runner/node-entry.ts')`.
|
|
262
264
|
/// [installHelper] — the offline law-install lane. An EXISTING `.mjs` file is run as a node script (the
|
|
@@ -308,13 +310,18 @@ class NomosTestHarness {
|
|
|
308
310
|
final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
|
|
309
311
|
// Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
|
|
310
312
|
NodeRunnerTransport? transport;
|
|
311
|
-
final phase = (String m) {
|
|
313
|
+
final phase = (String m) {
|
|
314
|
+
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
315
|
+
};
|
|
312
316
|
try {
|
|
313
317
|
final snapshot = '${workdir.path}/snapshot.bin';
|
|
314
318
|
final info = await _phased(
|
|
315
|
-
phase:
|
|
319
|
+
phase:
|
|
320
|
+
'1/4: offline law install (${deployJsonPaths.length} package(s))',
|
|
316
321
|
workspace: workspace,
|
|
317
|
-
awaiting: useInRepoScript
|
|
322
|
+
awaiting: useInRepoScript
|
|
323
|
+
? 'offline-install.ts'
|
|
324
|
+
: 'githolon harness-install',
|
|
318
325
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
319
326
|
onDiagnostic: onDiagnostic,
|
|
320
327
|
likelyCauses: const [
|
|
@@ -344,6 +351,11 @@ class NomosTestHarness {
|
|
|
344
351
|
environment: {
|
|
345
352
|
'NOMOS_OFFLINE': '1',
|
|
346
353
|
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
354
|
+
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
355
|
+
if (info['devicePublic'] != null)
|
|
356
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
|
|
357
|
+
if (info['deviceSecret'] != null)
|
|
358
|
+
'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
|
|
347
359
|
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
348
360
|
},
|
|
349
361
|
onDiagnostic: onDiagnostic,
|
|
@@ -355,7 +367,7 @@ class NomosTestHarness {
|
|
|
355
367
|
timeout: phaseTimeout ?? readyTimeout,
|
|
356
368
|
onDiagnostic: onDiagnostic,
|
|
357
369
|
likelyCauses: const [
|
|
358
|
-
'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
|
|
370
|
+
'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
|
|
359
371
|
'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
|
|
360
372
|
],
|
|
361
373
|
run: () => plane.ready);
|
|
@@ -370,7 +382,8 @@ class NomosTestHarness {
|
|
|
370
382
|
'run with onDiagnostic to see where the runner blocked',
|
|
371
383
|
],
|
|
372
384
|
run: () => plane.connect(
|
|
373
|
-
cloud:
|
|
385
|
+
cloud:
|
|
386
|
+
'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
374
387
|
workspace: workspace,
|
|
375
388
|
clientId: clientId,
|
|
376
389
|
allowUnborn: true,
|
|
@@ -379,7 +392,8 @@ class NomosTestHarness {
|
|
|
379
392
|
// with the SAME key, so offline `.births()` offers Just Work.
|
|
380
393
|
authorSecret: info['deviceSecret']?.toString(),
|
|
381
394
|
));
|
|
382
|
-
phase(
|
|
395
|
+
phase(
|
|
396
|
+
'ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
|
|
383
397
|
// THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
|
|
384
398
|
// single-workspace assumptions ship — with one session connected, an op that forgot its session
|
|
385
399
|
// silently works in tests and only refuses in production when the app connects its second
|
|
@@ -403,13 +417,15 @@ class NomosTestHarness {
|
|
|
403
417
|
restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
|
|
404
418
|
));
|
|
405
419
|
}
|
|
406
|
-
final harness = NomosTestHarness._(
|
|
407
|
-
|
|
420
|
+
final harness = NomosTestHarness._(plane.session(workspace), workspace,
|
|
421
|
+
transport, workdir, onDiagnostic, [workspace],
|
|
408
422
|
devicePublics: {
|
|
409
|
-
if (info['devicePublic'] != null)
|
|
423
|
+
if (info['devicePublic'] != null)
|
|
424
|
+
workspace: info['devicePublic'].toString(),
|
|
410
425
|
},
|
|
411
426
|
deviceSecrets: {
|
|
412
|
-
if (info['deviceSecret'] != null)
|
|
427
|
+
if (info['deviceSecret'] != null)
|
|
428
|
+
workspace: info['deviceSecret'].toString(),
|
|
413
429
|
},
|
|
414
430
|
frameworkHash: info['frameworkHash']?.toString(),
|
|
415
431
|
lawHashes: {
|
|
@@ -417,7 +433,9 @@ class NomosTestHarness {
|
|
|
417
433
|
.map((e) => e.toString())
|
|
418
434
|
.toList()
|
|
419
435
|
},
|
|
420
|
-
snapshots: {
|
|
436
|
+
snapshots: {
|
|
437
|
+
workspace: base64Encode(await File(snapshot).readAsBytes())
|
|
438
|
+
},
|
|
421
439
|
installLane: _InstallLane(
|
|
422
440
|
node: node,
|
|
423
441
|
useInRepoScript: useInRepoScript,
|
|
@@ -499,6 +517,8 @@ class NomosTestHarness {
|
|
|
499
517
|
final lawHashesByWs = <String, List<String>>{};
|
|
500
518
|
final installDevicePublics = <String, String>{};
|
|
501
519
|
final installDeviceSecrets = <String, String>{};
|
|
520
|
+
String? planeTestRootPublic;
|
|
521
|
+
String? planeTestRootSecret;
|
|
502
522
|
String? frameworkHash;
|
|
503
523
|
var _i = 0;
|
|
504
524
|
for (final e in workspaces.entries) {
|
|
@@ -506,7 +526,9 @@ class NomosTestHarness {
|
|
|
506
526
|
final info = await _phased(
|
|
507
527
|
phase: '1: offline law install ${++_i}/${workspaces.length}',
|
|
508
528
|
workspace: e.key,
|
|
509
|
-
awaiting: useInRepoScript
|
|
529
|
+
awaiting: useInRepoScript
|
|
530
|
+
? 'offline-install.ts'
|
|
531
|
+
: 'githolon harness-install',
|
|
510
532
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
511
533
|
onDiagnostic: onDiagnostic,
|
|
512
534
|
likelyCauses: const [
|
|
@@ -530,12 +552,18 @@ class NomosTestHarness {
|
|
|
530
552
|
frameworkHash ??= info['frameworkHash']?.toString();
|
|
531
553
|
// THE HARNESS DEVICE (era-0): the install lane mints one device key per workspace — sessions sign
|
|
532
554
|
// with it and the client completes the self-serve birth ceremony (enrol + cert) with the SAME key.
|
|
533
|
-
if (info['devicePublic'] != null)
|
|
534
|
-
|
|
555
|
+
if (info['devicePublic'] != null)
|
|
556
|
+
installDevicePublics[e.key] = info['devicePublic'].toString();
|
|
557
|
+
if (info['deviceSecret'] != null)
|
|
558
|
+
installDeviceSecrets[e.key] = info['deviceSecret'].toString();
|
|
559
|
+
planeTestRootPublic ??= info['devicePublic']?.toString();
|
|
560
|
+
planeTestRootSecret ??= info['deviceSecret']?.toString();
|
|
535
561
|
}
|
|
536
562
|
|
|
537
563
|
// 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
|
|
538
|
-
final phase = (String m) {
|
|
564
|
+
final phase = (String m) {
|
|
565
|
+
if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
|
|
566
|
+
};
|
|
539
567
|
transport = await _phased(
|
|
540
568
|
phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
|
|
541
569
|
awaiting: 'Process.start($node)',
|
|
@@ -547,6 +575,11 @@ class NomosTestHarness {
|
|
|
547
575
|
node: node,
|
|
548
576
|
environment: {
|
|
549
577
|
'NOMOS_OFFLINE': '1',
|
|
578
|
+
'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
|
|
579
|
+
if (planeTestRootPublic != null)
|
|
580
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
|
|
581
|
+
if (planeTestRootSecret != null)
|
|
582
|
+
'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
|
|
550
583
|
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
551
584
|
},
|
|
552
585
|
onDiagnostic: onDiagnostic,
|
|
@@ -558,7 +591,7 @@ class NomosTestHarness {
|
|
|
558
591
|
timeout: phaseTimeout ?? readyTimeout,
|
|
559
592
|
onDiagnostic: onDiagnostic,
|
|
560
593
|
likelyCauses: const [
|
|
561
|
-
'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
|
|
594
|
+
'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
|
|
562
595
|
'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
|
|
563
596
|
],
|
|
564
597
|
run: () => bridge.ready);
|
|
@@ -568,7 +601,8 @@ class NomosTestHarness {
|
|
|
568
601
|
final snapshotB64 = <String, String>{};
|
|
569
602
|
var _n = 0;
|
|
570
603
|
for (final e in workspaces.entries) {
|
|
571
|
-
snapshotB64[e.key] =
|
|
604
|
+
snapshotB64[e.key] =
|
|
605
|
+
base64Encode(await File(snapshots[e.key]!).readAsBytes());
|
|
572
606
|
await _phased(
|
|
573
607
|
phase: '3: connect session ${++_n}/${workspaces.length}',
|
|
574
608
|
workspace: e.key,
|
|
@@ -633,7 +667,8 @@ class NomosTestHarness {
|
|
|
633
667
|
));
|
|
634
668
|
}
|
|
635
669
|
if (deviceKeyed.isNotEmpty) {
|
|
636
|
-
phase(
|
|
670
|
+
phase(
|
|
671
|
+
'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened');
|
|
637
672
|
}
|
|
638
673
|
|
|
639
674
|
final names = workspaces.keys.toList();
|
|
@@ -714,7 +749,8 @@ class NomosTestHarness {
|
|
|
714
749
|
}
|
|
715
750
|
final entry = nodeEntry ??
|
|
716
751
|
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
717
|
-
final bareUid =
|
|
752
|
+
final bareUid =
|
|
753
|
+
subject.startsWith('user:') ? subject.substring(5) : subject;
|
|
718
754
|
final http = HttpClient();
|
|
719
755
|
Future<Map<String, Object?>> post(String path, Object? body,
|
|
720
756
|
{Map<String, String> headers = const {}}) async {
|
|
@@ -723,7 +759,8 @@ class NomosTestHarness {
|
|
|
723
759
|
req.headers.set('x-nomos-principal', bareUid);
|
|
724
760
|
if (authToken != null) req.headers.set('x-nomos-auth', authToken);
|
|
725
761
|
headers.forEach(req.headers.set);
|
|
726
|
-
if (body != null)
|
|
762
|
+
if (body != null)
|
|
763
|
+
req.add(utf8.encode(body is String ? body : jsonEncode(body)));
|
|
727
764
|
final resp = await req.close();
|
|
728
765
|
final text = await resp.transform(utf8.decoder).join();
|
|
729
766
|
final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
|
|
@@ -790,7 +827,10 @@ class NomosTestHarness {
|
|
|
790
827
|
timeout: phaseTimeout ?? _kSpawnDeadline,
|
|
791
828
|
onDiagnostic: onDiagnostic,
|
|
792
829
|
run: () => NodeRunnerTransport.start(
|
|
793
|
-
entry: entry,
|
|
830
|
+
entry: entry,
|
|
831
|
+
resolveFrom: resolveFrom,
|
|
832
|
+
node: node,
|
|
833
|
+
onDiagnostic: onDiagnostic));
|
|
794
834
|
final plane = NomosBridge(transport!);
|
|
795
835
|
await _phased(
|
|
796
836
|
phase: '3/6: runner ready',
|
|
@@ -804,7 +844,8 @@ class NomosTestHarness {
|
|
|
804
844
|
// on-chain AuthProvider (enrollFirstDevice — no admin in the loop). The platform is WARRANTED, so
|
|
805
845
|
// plain unsigned enrolment refuses by design; this is the one first-device door.
|
|
806
846
|
final device = await _phased(
|
|
807
|
-
phase:
|
|
847
|
+
phase:
|
|
848
|
+
'4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
|
|
808
849
|
workspace: platformWorkspace,
|
|
809
850
|
awaiting: 'authorKeygen + /dev/mint-token + enrollFirstDevice',
|
|
810
851
|
timeout: phaseTimeout ?? _kInstallDeadline,
|
|
@@ -826,14 +867,17 @@ class NomosTestHarness {
|
|
|
826
867
|
final publicB64 = (kp['public'] ?? '').toString();
|
|
827
868
|
final secret = (kp['secret'] ?? '').toString();
|
|
828
869
|
if (publicB64.isEmpty || secret.isEmpty) {
|
|
829
|
-
throw NomosTestHarnessException(
|
|
870
|
+
throw NomosTestHarnessException(
|
|
871
|
+
'cloudBound: authorKeygen returned no device key');
|
|
830
872
|
}
|
|
831
873
|
final keyHash = sha256.convert(base64Decode(publicB64)).toString();
|
|
832
|
-
final minted = await post(
|
|
874
|
+
final minted = await post(
|
|
875
|
+
'/dev/mint-token', {'sub': bareUid, 'nonce': keyHash});
|
|
833
876
|
final idpToken = (minted['token'] ?? '').toString();
|
|
834
877
|
final issuer = (minted['issuer'] ?? '').toString();
|
|
835
878
|
if (idpToken.isEmpty || issuer.isEmpty) {
|
|
836
|
-
throw NomosTestHarnessException(
|
|
879
|
+
throw NomosTestHarnessException(
|
|
880
|
+
'cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?');
|
|
837
881
|
}
|
|
838
882
|
// THE KEYLESS ENROLL-ONCE DELEGATION (links[1] of the home cert chain) — device-self-signed,
|
|
839
883
|
// its authority the dev-IdP token. enrollFirstDevice SEEDS it as the user's SignerDelegation, so
|
|
@@ -860,8 +904,15 @@ class NomosTestHarness {
|
|
|
860
904
|
'actor': subject,
|
|
861
905
|
'authorSecret': secret,
|
|
862
906
|
'delegationCert': jsonEncode(delegationCert),
|
|
863
|
-
}, headers: {
|
|
864
|
-
|
|
907
|
+
}, headers: {
|
|
908
|
+
'x-nomos-auth': idpToken
|
|
909
|
+
});
|
|
910
|
+
return (
|
|
911
|
+
publicB64: publicB64,
|
|
912
|
+
secret: secret,
|
|
913
|
+
keyHash: keyHash,
|
|
914
|
+
idpToken: idpToken
|
|
915
|
+
);
|
|
865
916
|
});
|
|
866
917
|
// 5/6 — THE HOME CEREMONY, remotely: the platform's own gateway offer births the home (the ONE
|
|
867
918
|
// control-plane contact; the app lane below never touches the platform again). The offer carries
|
|
@@ -886,7 +937,8 @@ class NomosTestHarness {
|
|
|
886
937
|
'parentId': platformWorkspace,
|
|
887
938
|
'epoch': 0,
|
|
888
939
|
}, device.secret);
|
|
889
|
-
final out =
|
|
940
|
+
final out =
|
|
941
|
+
await post('/v2/workspaces/$platformWorkspace/birthHome', {
|
|
890
942
|
'owner': subject,
|
|
891
943
|
'ownerKeyHash': device.keyHash,
|
|
892
944
|
'frameworkHash': frameworkHash,
|
|
@@ -895,9 +947,15 @@ class NomosTestHarness {
|
|
|
895
947
|
'homeCert': jsonEncode(homeCert),
|
|
896
948
|
'actor': subject,
|
|
897
949
|
'authorSecret': device.secret,
|
|
898
|
-
}, headers: {
|
|
950
|
+
}, headers: {
|
|
951
|
+
'x-nomos-auth': device.idpToken
|
|
952
|
+
});
|
|
899
953
|
final born = (out['born'] as List?) ?? const [];
|
|
900
|
-
final bornNames = born
|
|
954
|
+
final bornNames = born
|
|
955
|
+
.map((b) => (b as Map)['workspace']?.toString())
|
|
956
|
+
.whereType<String>()
|
|
957
|
+
.where((w) => w.isNotEmpty)
|
|
958
|
+
.toList();
|
|
901
959
|
home = bornNames.isEmpty ? null : bornNames.first;
|
|
902
960
|
if (home == null) {
|
|
903
961
|
final outcomes = jsonEncode(out['birthOutcomes'] ?? const []);
|
|
@@ -927,7 +985,8 @@ class NomosTestHarness {
|
|
|
927
985
|
// NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
|
|
928
986
|
// locally-born child), so the plane is never sole-session — the decoy the offline harness
|
|
929
987
|
// needs (to break single-session inference) would just be a spurious local-only workspace here.
|
|
930
|
-
final _ =
|
|
988
|
+
final _ =
|
|
989
|
+
decoySession; // (accepted for signature parity; a cloudBound plane needs no decoy)
|
|
931
990
|
final harness = NomosTestHarness._(
|
|
932
991
|
plane.session(home!),
|
|
933
992
|
home!,
|
|
@@ -951,8 +1010,12 @@ class NomosTestHarness {
|
|
|
951
1010
|
}
|
|
952
1011
|
return harness;
|
|
953
1012
|
} catch (e) {
|
|
954
|
-
try {
|
|
955
|
-
|
|
1013
|
+
try {
|
|
1014
|
+
transport?.dispose();
|
|
1015
|
+
} catch (_) {}
|
|
1016
|
+
try {
|
|
1017
|
+
await workdir.delete(recursive: true);
|
|
1018
|
+
} catch (_) {}
|
|
956
1019
|
rethrow;
|
|
957
1020
|
} finally {
|
|
958
1021
|
http.close(force: true);
|
|
@@ -970,11 +1033,13 @@ class NomosTestHarness {
|
|
|
970
1033
|
/// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
|
|
971
1034
|
/// per-workspace last verdicts). Returns the final sync result per workspace.
|
|
972
1035
|
Future<Map<String, Map<String, Object?>>> converge(
|
|
973
|
-
{Duration timeout = const Duration(seconds: 120),
|
|
1036
|
+
{Duration timeout = const Duration(seconds: 120),
|
|
1037
|
+
List<String>? workspaces}) async {
|
|
974
1038
|
// Every LIVE session on the plane (not the harness's construction-time list — a locally-born child
|
|
975
1039
|
// acquired after construction must converge too), PARENT-FIRST: the home syncs before its children, so
|
|
976
1040
|
// its birth offer-effect creates each child's remote custody BEFORE that child pushes its own writes.
|
|
977
|
-
final live =
|
|
1041
|
+
final live =
|
|
1042
|
+
bridge.connectedSessions.where((w) => w != _decoyWorkspace).toList();
|
|
978
1043
|
final hw = homeWorkspace;
|
|
979
1044
|
if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
|
|
980
1045
|
final targets = workspaces ?? live;
|
|
@@ -984,9 +1049,11 @@ class NomosTestHarness {
|
|
|
984
1049
|
while (pending.isNotEmpty) {
|
|
985
1050
|
for (final ws in List.of(pending)) {
|
|
986
1051
|
try {
|
|
987
|
-
final r = (await bridge.session(ws).sync(admit: true))
|
|
1052
|
+
final r = (await bridge.session(ws).sync(admit: true))
|
|
1053
|
+
.cast<String, Object?>();
|
|
988
1054
|
last[ws] = r;
|
|
989
|
-
if (r['pushFailed'] != true && r['stillAhead'] != true)
|
|
1055
|
+
if (r['pushFailed'] != true && r['stillAhead'] != true)
|
|
1056
|
+
pending.remove(ws);
|
|
990
1057
|
} catch (e) {
|
|
991
1058
|
last[ws] = {'error': e.toString()};
|
|
992
1059
|
}
|
|
@@ -1056,7 +1123,9 @@ class NomosTestHarness {
|
|
|
1056
1123
|
final info = await _phased(
|
|
1057
1124
|
phase: 'seed: offline law install',
|
|
1058
1125
|
workspace: workspace,
|
|
1059
|
-
awaiting: lane.useInRepoScript
|
|
1126
|
+
awaiting: lane.useInRepoScript
|
|
1127
|
+
? 'offline-install.ts'
|
|
1128
|
+
: 'githolon harness-install',
|
|
1060
1129
|
timeout: _phaseTimeout ?? _kInstallDeadline,
|
|
1061
1130
|
onDiagnostic: _onDiagnostic,
|
|
1062
1131
|
run: () => _runInstall(
|
|
@@ -1119,7 +1188,8 @@ class NomosTestHarness {
|
|
|
1119
1188
|
/// un-synced writes + the DLQ), with its head + law hashes + capture time. The bytes are the SAME
|
|
1120
1189
|
/// base64 `connect(restoreFrom:)` consumes — [restoreWorkspace] round-trips it. O(1)-ish (a tree serialize).
|
|
1121
1190
|
Future<WorkspaceSnapshot> snapshotWorkspace(String workspace) async {
|
|
1122
|
-
final s = session(
|
|
1191
|
+
final s = session(
|
|
1192
|
+
workspace); // throws NomosTestHarnessException if not a connected session
|
|
1123
1193
|
return _phased(
|
|
1124
1194
|
phase: 'snapshotWorkspace',
|
|
1125
1195
|
workspace: workspace,
|
|
@@ -1147,13 +1217,15 @@ class NomosTestHarness {
|
|
|
1147
1217
|
for (final ws in _sessionNames) {
|
|
1148
1218
|
out[ws] = await snapshotWorkspace(ws);
|
|
1149
1219
|
}
|
|
1150
|
-
return PlaneSnapshot(
|
|
1220
|
+
return PlaneSnapshot(
|
|
1221
|
+
workspaces: Map.unmodifiable(out), capturedAt: DateTime.now().toUtc());
|
|
1151
1222
|
}
|
|
1152
1223
|
|
|
1153
1224
|
/// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
|
|
1154
1225
|
/// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
|
|
1155
1226
|
/// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
|
|
1156
|
-
Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
|
|
1227
|
+
Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
|
|
1228
|
+
{String? into}) async {
|
|
1157
1229
|
final ws = into ?? snapshot.workspace;
|
|
1158
1230
|
await _phased(
|
|
1159
1231
|
phase: 'restoreWorkspace',
|
|
@@ -1191,7 +1263,8 @@ class NomosTestHarness {
|
|
|
1191
1263
|
if (_disposed) return;
|
|
1192
1264
|
_disposed = true;
|
|
1193
1265
|
final f = _onDiagnostic;
|
|
1194
|
-
if (f != null)
|
|
1266
|
+
if (f != null)
|
|
1267
|
+
f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
|
|
1195
1268
|
try {
|
|
1196
1269
|
await _transport.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
|
|
1197
1270
|
onTimeout: () => throw HarnessPhaseTimeout(
|
|
@@ -1200,7 +1273,8 @@ class NomosTestHarness {
|
|
|
1200
1273
|
elapsed: _phaseTimeout ?? _kOtherDeadline,
|
|
1201
1274
|
awaiting: 'NodeRunnerTransport.dispose'));
|
|
1202
1275
|
} catch (e) {
|
|
1203
|
-
_diag(
|
|
1276
|
+
_diag(
|
|
1277
|
+
'[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
|
|
1204
1278
|
}
|
|
1205
1279
|
try {
|
|
1206
1280
|
if (await _workdir.exists()) await _workdir.delete(recursive: true);
|
|
@@ -1235,9 +1309,12 @@ class NomosTestHarness {
|
|
|
1235
1309
|
// Both lanes take the SAME flags (--deploy/--workspace/--out/--runtime) and print the SAME one-line JSON
|
|
1236
1310
|
// contract; only the leading command differs (node <script> vs `<githolon...> harness-install`).
|
|
1237
1311
|
final common = <String>[
|
|
1238
|
-
'--deploy',
|
|
1239
|
-
'
|
|
1240
|
-
'--
|
|
1312
|
+
'--deploy',
|
|
1313
|
+
deployJsonPaths.join(','),
|
|
1314
|
+
'--workspace',
|
|
1315
|
+
workspace,
|
|
1316
|
+
'--out',
|
|
1317
|
+
snapshot,
|
|
1241
1318
|
if (runtimeDir != null) ...['--runtime', runtimeDir],
|
|
1242
1319
|
];
|
|
1243
1320
|
final String executable;
|
|
@@ -1247,8 +1324,11 @@ class NomosTestHarness {
|
|
|
1247
1324
|
args = [inRepoScript, ...common];
|
|
1248
1325
|
} else {
|
|
1249
1326
|
// `githolonCmd` is a command line (e.g. `npx githolon` or `node /abs/cli.mjs`) — split into tokens.
|
|
1250
|
-
final tokens =
|
|
1251
|
-
|
|
1327
|
+
final tokens = githolonCmd
|
|
1328
|
+
.trim()
|
|
1329
|
+
.split(RegExp(r'\s+'))
|
|
1330
|
+
.where((t) => t.isNotEmpty)
|
|
1331
|
+
.toList();
|
|
1252
1332
|
if (tokens.isEmpty) {
|
|
1253
1333
|
throw NomosTestHarnessException(
|
|
1254
1334
|
'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)');
|
|
@@ -1335,12 +1415,15 @@ class WorkspaceSnapshot {
|
|
|
1335
1415
|
'capturedAt': capturedAt.toUtc().toIso8601String(),
|
|
1336
1416
|
};
|
|
1337
1417
|
|
|
1338
|
-
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
1418
|
+
factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
|
|
1419
|
+
WorkspaceSnapshot(
|
|
1339
1420
|
workspace: j['workspace'] as String,
|
|
1340
1421
|
snapshotB64: j['snapshotB64'] as String,
|
|
1341
1422
|
head: (j['head'] ?? '') as String,
|
|
1342
|
-
lawHashes:
|
|
1343
|
-
|
|
1423
|
+
lawHashes:
|
|
1424
|
+
((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
|
|
1425
|
+
capturedAt: DateTime.parse((j['capturedAt'] ??
|
|
1426
|
+
DateTime.now().toUtc().toIso8601String()) as String),
|
|
1344
1427
|
);
|
|
1345
1428
|
|
|
1346
1429
|
@override
|
|
@@ -1363,8 +1446,12 @@ class PlaneSnapshot {
|
|
|
1363
1446
|
};
|
|
1364
1447
|
|
|
1365
1448
|
factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
|
|
1366
|
-
workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
|
|
1367
|
-
|
|
1368
|
-
|
|
1449
|
+
workspaces: ((j['workspaces'] as Map?) ?? const {}).map((k, v) =>
|
|
1450
|
+
MapEntry(
|
|
1451
|
+
'$k',
|
|
1452
|
+
WorkspaceSnapshot.fromJson(
|
|
1453
|
+
(v as Map).cast<String, Object?>()))),
|
|
1454
|
+
capturedAt: DateTime.parse((j['capturedAt'] ??
|
|
1455
|
+
DateTime.now().toUtc().toIso8601String()) as String),
|
|
1369
1456
|
);
|
|
1370
1457
|
}
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.62.
|
|
14
|
+
version: 0.62.2
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -29,7 +29,7 @@ String? _missingPrereq() {
|
|
|
29
29
|
return 'missing fixture $_docsDeploy — compile it: '
|
|
30
30
|
'(cd bench/scale/fixtures/surfaced && node ../../../../dsl/compile_package.mjs)';
|
|
31
31
|
}
|
|
32
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
32
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
33
33
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
34
34
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
35
35
|
}
|
|
@@ -37,7 +37,7 @@ String? _missingPrereq() {
|
|
|
37
37
|
for (final p in [_repoEntry, _installer, _guestbook, _surfaced, _attributed, _estateBirth]) {
|
|
38
38
|
if (!File(p).existsSync()) return 'missing $p (compile the fixture / example first)';
|
|
39
39
|
}
|
|
40
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
40
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
41
41
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
42
42
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
43
43
|
}
|
|
@@ -42,7 +42,7 @@ String? _missingPrereq() {
|
|
|
42
42
|
'(needs `dart: { support: "package" }` in nomos.package.mjs) — missing $p';
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
45
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
46
46
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
47
47
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
48
48
|
}
|
|
@@ -37,7 +37,7 @@ String? _missingPrereq() {
|
|
|
37
37
|
if (!File(_birthDeploy).existsSync()) {
|
|
38
38
|
return 'compile the fixture: cd bench/scale/fixtures/estate-birth && node ../../../../dsl/compile_package.mjs — missing $_birthDeploy';
|
|
39
39
|
}
|
|
40
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
40
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
41
41
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
42
42
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
43
43
|
}
|
|
@@ -27,7 +27,7 @@ String? _missingPrereq() {
|
|
|
27
27
|
for (final p in [_catDeploy, _estateDeploy]) {
|
|
28
28
|
if (!File(p).existsSync()) return 'missing fixture $p (compile bench/scale/fixtures first)';
|
|
29
29
|
}
|
|
30
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
30
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
31
31
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
32
32
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
33
33
|
}
|
|
@@ -24,7 +24,7 @@ String? _missingPrereq() {
|
|
|
24
24
|
if (!File(_guestbookDeploy).existsSync()) {
|
|
25
25
|
return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
|
|
26
26
|
}
|
|
27
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
27
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
28
28
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
29
29
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
30
30
|
}
|
|
@@ -28,7 +28,7 @@ String? _missingPrereq() {
|
|
|
28
28
|
if (!File(_guestbookDeploy).existsSync()) {
|
|
29
29
|
return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
|
|
30
30
|
}
|
|
31
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
31
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
32
32
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
33
33
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
34
34
|
}
|
|
@@ -36,7 +36,7 @@ String? _missingPrereq() {
|
|
|
36
36
|
return 'missing fixture ${e.key} — compile it: (cd ${e.value} && node ../../../../dsl/compile_package.mjs)';
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
39
|
+
if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
|
|
40
40
|
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
41
41
|
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
42
42
|
}
|