@githolon/dsl 0.100.5 → 0.101.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 +12 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +220 -71
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- 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/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +132 -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/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +165 -29
- package/src/births.ts +53 -22
- package/src/build_package.ts +96 -20
- package/src/business_birth.ts +79 -16
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +930 -87
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +172 -53
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +61 -5
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
package/dart/pubspec.lock
CHANGED
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.
|
|
14
|
+
version: 0.63.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -20,7 +20,7 @@ dependencies:
|
|
|
20
20
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
21
21
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
22
22
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
23
|
-
nomos_types: ^0.
|
|
23
|
+
nomos_types: ^0.7.0
|
|
24
24
|
json_schema: ^5.2.2
|
|
25
25
|
crypto: ^3.0.0
|
|
26
26
|
|
|
@@ -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
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// THE VERSION-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// check can neither lag (missing real staleness) nor lead (refusing the current bundle).
|
|
1
|
+
// THE VERSION-FLOOR TRIPWIRE: the @githolon/client built by this checkout must satisfy the minimum
|
|
2
|
+
// runner version declared by nomos_client. Changesets may advance the npm package metadata without
|
|
3
|
+
// changing its protocol, so a newer runner is valid; an older runner than the Dart floor is not.
|
|
5
4
|
import 'dart:convert';
|
|
6
5
|
import 'dart:io';
|
|
7
6
|
|
|
@@ -16,25 +15,15 @@ void main() {
|
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
final v = (jsonDecode(f.readAsStringSync()) as Map)['version'] as String;
|
|
19
|
-
if (kExpectedRunnerBundleVersion == v) return; // released state: exact pair
|
|
20
|
-
// PENDING-RELEASE state: the floor may lead package.json by exactly the bump a queued
|
|
21
|
-
// changeset will apply (the changesets flow bumps package.json only when the version PR
|
|
22
|
-
// merges). Anything else — lagging, or leading with no changeset queued — is drift.
|
|
23
|
-
final changesets = Directory('${Directory.current.path}/../../.changeset')
|
|
24
|
-
.listSync()
|
|
25
|
-
.whereType<File>()
|
|
26
|
-
.where((c) => c.path.endsWith('.md') && !c.path.endsWith('README.md'))
|
|
27
|
-
.where((c) => c.readAsStringSync().contains('@githolon/client'));
|
|
28
|
-
expect(changesets, isNotEmpty,
|
|
29
|
-
reason: 'kExpectedRunnerBundleVersion ($kExpectedRunnerBundleVersion) != @githolon/client '
|
|
30
|
-
'($v) and NO client changeset is queued — bump the floor WITH the client release '
|
|
31
|
-
'(the stale-bundle floor must move with the bundle it ships beside)');
|
|
32
18
|
List<int> parse(String x) =>
|
|
33
19
|
x.split('.').map((p) => int.tryParse(p) ?? 0).toList();
|
|
34
|
-
final
|
|
35
|
-
final
|
|
36
|
-
(
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
final floor = parse(kExpectedRunnerBundleVersion), runner = parse(v);
|
|
21
|
+
final runnerMeetsFloor = runner[0] > floor[0] ||
|
|
22
|
+
(runner[0] == floor[0] &&
|
|
23
|
+
(runner[1] > floor[1] ||
|
|
24
|
+
(runner[1] == floor[1] && runner[2] >= floor[2])));
|
|
25
|
+
expect(runnerMeetsFloor, isTrue,
|
|
26
|
+
reason: '@githolon/client $v is older than nomos_client runner floor '
|
|
27
|
+
'$kExpectedRunnerBundleVersion');
|
|
39
28
|
});
|
|
40
29
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Product acceptance: application → home → Estate, then Site → Asset, through the generated Dart client.
|
|
2
|
+
// The harness owns the test cloud and framework ceremony; the application section supplies business facts.
|
|
3
|
+
import 'dart:convert';
|
|
4
|
+
import 'dart:io';
|
|
5
|
+
|
|
6
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
7
|
+
import 'package:test/test.dart';
|
|
8
|
+
|
|
9
|
+
import '../../../examples/co2-birth/build/dart/src/generated/co2.client.dart';
|
|
10
|
+
|
|
11
|
+
String get _root => '${Directory.current.path}/../..';
|
|
12
|
+
String get _runner =>
|
|
13
|
+
Platform.environment['NOMOS_NODE_ENTRY'] ??
|
|
14
|
+
'$_root/cloud/web-client/runner/node-entry.ts';
|
|
15
|
+
String get _server => '$_root/cloud/dev-server/server.ts';
|
|
16
|
+
String get _deploy =>
|
|
17
|
+
Platform.environment['NOMOS_CO2_DEPLOY'] ??
|
|
18
|
+
'$_root/examples/co2-birth/build/co2.deploy.json';
|
|
19
|
+
const _subject = 'user:co2-product-owner';
|
|
20
|
+
|
|
21
|
+
Future<T> _eventually<T>(
|
|
22
|
+
String description,
|
|
23
|
+
Future<T?> Function() probe, {
|
|
24
|
+
Duration timeout = const Duration(seconds: 90),
|
|
25
|
+
}) async {
|
|
26
|
+
final deadline = DateTime.now().add(timeout);
|
|
27
|
+
Object? lastError;
|
|
28
|
+
while (DateTime.now().isBefore(deadline)) {
|
|
29
|
+
try {
|
|
30
|
+
final value = await probe();
|
|
31
|
+
if (value != null) return value;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
}
|
|
35
|
+
await Future<void>.delayed(const Duration(milliseconds: 250));
|
|
36
|
+
}
|
|
37
|
+
throw TestFailure(
|
|
38
|
+
'$description did not become true within ${timeout.inSeconds}s${lastError == null ? '' : ': $lastError'}');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void main() {
|
|
42
|
+
test(
|
|
43
|
+
'CO2 product journey: application → home → Estate → Site → Asset, offline then remotely durable',
|
|
44
|
+
() async {
|
|
45
|
+
final required = Platform.environment['NOMOS_GAUNTLET_REQUIRED'] == '1';
|
|
46
|
+
final missing = [_runner, _server, _deploy]
|
|
47
|
+
.where((path) => !File(path).existsSync())
|
|
48
|
+
.toList();
|
|
49
|
+
if (missing.isNotEmpty) {
|
|
50
|
+
if (required)
|
|
51
|
+
fail(
|
|
52
|
+
'CO2 product gauntlet prerequisites are missing: ${missing.join(', ')}');
|
|
53
|
+
markTestSkipped(
|
|
54
|
+
'CO2 product gauntlet prerequisites are missing: ${missing.join(', ')}');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
final diagnostic = Platform.environment['NOMOS_HARNESS_DIAG'] == '1'
|
|
59
|
+
? (String line) => stderr.writeln(line)
|
|
60
|
+
: null;
|
|
61
|
+
final cloud = await NomosTestCloud.start(
|
|
62
|
+
serverEntry: _server,
|
|
63
|
+
platform: 'co2-product-platform',
|
|
64
|
+
deployJsonPath: _deploy,
|
|
65
|
+
onDiagnostic: diagnostic,
|
|
66
|
+
);
|
|
67
|
+
NomosTestHarness? harness;
|
|
68
|
+
try {
|
|
69
|
+
// Release coherence is part of sync correctness, not an optional diagnostic. If the cloud cannot
|
|
70
|
+
// identify the kernel that will receive these ledgers, a successful local write is not an adequate
|
|
71
|
+
// product proof: the client would be synchronising into an unknown fold implementation.
|
|
72
|
+
final capabilitiesHttp = HttpClient();
|
|
73
|
+
try {
|
|
74
|
+
final request = await capabilitiesHttp
|
|
75
|
+
.getUrl(Uri.parse('${cloud.origin}/v1/runtime/capabilities'));
|
|
76
|
+
final response = await request.close();
|
|
77
|
+
final text = await response.transform(utf8.decoder).join();
|
|
78
|
+
expect(response.statusCode, HttpStatus.ok,
|
|
79
|
+
reason:
|
|
80
|
+
'the cloud must advertise the kernel that will receive offline ledgers: $text');
|
|
81
|
+
final capabilities = (jsonDecode(text) as Map).cast<String, Object?>();
|
|
82
|
+
final production =
|
|
83
|
+
(capabilities['prod'] as Map?)?.cast<String, Object?>() ?? const {};
|
|
84
|
+
final features = ((production['features'] as List?) ?? const [])
|
|
85
|
+
.map((feature) => feature.toString())
|
|
86
|
+
.toSet();
|
|
87
|
+
expect(
|
|
88
|
+
features,
|
|
89
|
+
containsAll(<String>[
|
|
90
|
+
'conflictSnapshots',
|
|
91
|
+
'ownedAudience',
|
|
92
|
+
]),
|
|
93
|
+
reason:
|
|
94
|
+
'the receiving kernel must support the law we are about to sync');
|
|
95
|
+
} finally {
|
|
96
|
+
capabilitiesHttp.close(force: true);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
harness = await NomosTestHarness.cloudBound(
|
|
100
|
+
cloud: cloud,
|
|
101
|
+
deployJsonPaths: [_deploy],
|
|
102
|
+
platformWorkspace: 'co2-product-platform',
|
|
103
|
+
subject: _subject,
|
|
104
|
+
nodeEntry: _runner,
|
|
105
|
+
onDiagnostic: diagnostic,
|
|
106
|
+
phaseTimeout: const Duration(seconds: 180),
|
|
107
|
+
);
|
|
108
|
+
final home = harness.homeSession;
|
|
109
|
+
expect((await cloud.remoteHead(home.workspace)).initialized, isTrue,
|
|
110
|
+
reason:
|
|
111
|
+
'the application born the identity home before business code ran');
|
|
112
|
+
// The shipped Flutter scope starts this automatically. The headless acceptance test calls the same
|
|
113
|
+
// client-owned lifecycle explicitly because it has no widget scope; no later sync/converge call is
|
|
114
|
+
// permitted. The 60s fallback remains deliberately large: recovery must come from retry/reconnect,
|
|
115
|
+
// not a test-only tight poll.
|
|
116
|
+
await home.autoSync(fallbackMs: 60000);
|
|
117
|
+
|
|
118
|
+
cloud.setOnline(false);
|
|
119
|
+
|
|
120
|
+
// This is the whole tenant journey. Every argument is a business fact; Nomos owns the offline birth,
|
|
121
|
+
// child acquisition, authorship, persistence, routing, retry and eventual parent-first custody.
|
|
122
|
+
final homeClient = await Co2Client.bind(home, actor: _subject);
|
|
123
|
+
final estateRef = await homeClient.birth(NomosBirthEstatePayload(
|
|
124
|
+
reference: 'E-001',
|
|
125
|
+
name: 'Foundry',
|
|
126
|
+
country: 'GB',
|
|
127
|
+
baselineTonnesCo2e: 4200,
|
|
128
|
+
));
|
|
129
|
+
final estate = await harness.bridge.follow(
|
|
130
|
+
estateRef,
|
|
131
|
+
authorSecret: harness.deviceSecret(home.workspace),
|
|
132
|
+
);
|
|
133
|
+
await estate.autoSync(fallbackMs: 60000);
|
|
134
|
+
final estateClient = await Co2Client.bind(estate, actor: _subject);
|
|
135
|
+
final site = await estateClient.addSite(
|
|
136
|
+
AddSitePayload(estateId: estateRef.id, name: 'Trafford Depot'),
|
|
137
|
+
);
|
|
138
|
+
final siteId = site.created.single.id;
|
|
139
|
+
await estateClient.addAsset(AddAssetPayload(
|
|
140
|
+
siteId: siteId,
|
|
141
|
+
name: 'Main electricity meter',
|
|
142
|
+
operationalStatus: 'commissioned',
|
|
143
|
+
));
|
|
144
|
+
final localAssets = await estateClient.assetsBySite(siteId: siteId);
|
|
145
|
+
expect(localAssets.single.name, 'Main electricity meter');
|
|
146
|
+
expect(localAssets.single.operationalStatus?.valueOrNull, 'commissioned');
|
|
147
|
+
|
|
148
|
+
expect((await cloud.remoteHead(estateRef.workspace)).initialized, isFalse,
|
|
149
|
+
reason:
|
|
150
|
+
'the Estate remained fully usable without remote custody during the partition');
|
|
151
|
+
|
|
152
|
+
cloud.setOnline(true);
|
|
153
|
+
// No tenant sync(), no harness.converge(), no topology ordering. The two stateful client sessions must
|
|
154
|
+
// retain their local branches, retry after reconnect, establish the parent birth first as required by
|
|
155
|
+
// the law, and finish child custody themselves.
|
|
156
|
+
await _eventually('client-owned parent and child convergence', () async {
|
|
157
|
+
final homeRemote = await cloud.remoteHead(home.workspace);
|
|
158
|
+
final estateRemote = await cloud.remoteHead(estateRef.workspace);
|
|
159
|
+
final homeMetrics = await home.metrics();
|
|
160
|
+
final estateMetrics = await estate.metrics();
|
|
161
|
+
final homeAhead = (homeMetrics['ahead'] as num?)?.toInt() ?? -1;
|
|
162
|
+
final estateAhead = (estateMetrics['ahead'] as num?)?.toInt() ?? -1;
|
|
163
|
+
if (homeRemote.initialized &&
|
|
164
|
+
estateRemote.initialized &&
|
|
165
|
+
homeAhead == 0 &&
|
|
166
|
+
estateAhead == 0) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}, timeout: const Duration(seconds: 180));
|
|
171
|
+
|
|
172
|
+
final http = HttpClient();
|
|
173
|
+
try {
|
|
174
|
+
final request = await http.getUrl(Uri.parse(
|
|
175
|
+
'${cloud.origin}/v2/workspaces/${estateRef.workspace}/assetsBySite?siteId=${Uri.encodeQueryComponent(siteId)}',
|
|
176
|
+
));
|
|
177
|
+
final response = await request.close();
|
|
178
|
+
final body =
|
|
179
|
+
(jsonDecode(await response.transform(utf8.decoder).join()) as Map)
|
|
180
|
+
.cast<String, Object?>();
|
|
181
|
+
final rows = (body['rows'] as List?) ?? const [];
|
|
182
|
+
expect(rows, hasLength(1));
|
|
183
|
+
expect(((rows.single as Map)['data'] as Map)['name'],
|
|
184
|
+
'Main electricity meter');
|
|
185
|
+
} finally {
|
|
186
|
+
http.close(force: true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
final remoteEstate = await cloud.remoteHead(estateRef.workspace);
|
|
190
|
+
expect(remoteEstate.initialized, isTrue);
|
|
191
|
+
expect(remoteEstate.commits, greaterThan(0));
|
|
192
|
+
|
|
193
|
+
// The serving cloud is not the proof of replayability. Start a fresh runner with no resident custody,
|
|
194
|
+
// clone the Estate from remote custody, and re-fire the complete authority/law chain locally.
|
|
195
|
+
final coldTransport = await NodeRunnerTransport.start(
|
|
196
|
+
entry: _runner,
|
|
197
|
+
environment: cloud.testKRootPublic == null
|
|
198
|
+
? null
|
|
199
|
+
: {
|
|
200
|
+
...Platform.environment,
|
|
201
|
+
'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
final cold = NomosBridge(coldTransport);
|
|
205
|
+
try {
|
|
206
|
+
await cold.ready;
|
|
207
|
+
await cold.connect(
|
|
208
|
+
cloud: cloud.origin,
|
|
209
|
+
workspace: estateRef.workspace,
|
|
210
|
+
clientId: 'co2-product-cold-peer',
|
|
211
|
+
);
|
|
212
|
+
final coldSession = cold.session(estateRef.workspace);
|
|
213
|
+
final coldClient = await Co2Client.bind(coldSession, actor: _subject);
|
|
214
|
+
expect((await coldClient.assetsBySite(siteId: siteId)).single.name,
|
|
215
|
+
'Main electricity meter');
|
|
216
|
+
expect((await coldSession.verifyChain())['valid'], isTrue);
|
|
217
|
+
} finally {
|
|
218
|
+
await cold.dispose();
|
|
219
|
+
await coldTransport.dispose();
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
try {
|
|
223
|
+
await harness?.dispose();
|
|
224
|
+
} catch (_) {}
|
|
225
|
+
await cloud.stop();
|
|
226
|
+
}
|
|
227
|
+
}, timeout: const Timeout(Duration(minutes: 7)));
|
|
228
|
+
}
|
|
@@ -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
|
}
|
|
@@ -4,11 +4,45 @@
|
|
|
4
4
|
import 'package:nomos_client/nomos_client.dart';
|
|
5
5
|
import 'package:test/test.dart';
|
|
6
6
|
|
|
7
|
-
NomosErrorInfo info(String code,
|
|
8
|
-
|
|
7
|
+
NomosErrorInfo info(String code,
|
|
8
|
+
{Map<String, Object?> attrs = const {},
|
|
9
|
+
String? workspace,
|
|
10
|
+
String? operation}) =>
|
|
11
|
+
NomosErrorInfo(
|
|
12
|
+
code: code,
|
|
13
|
+
message: 'refused',
|
|
14
|
+
workspace: workspace,
|
|
15
|
+
operation: operation,
|
|
16
|
+
attributes: attrs);
|
|
9
17
|
|
|
10
18
|
void main() {
|
|
11
|
-
test(
|
|
19
|
+
test(
|
|
20
|
+
'owned recovery mints one typed resumable state with the private protocol hidden',
|
|
21
|
+
() {
|
|
22
|
+
final e = nomosErrorFromInfo(info(
|
|
23
|
+
'nomos.owned.recovery_pending',
|
|
24
|
+
workspace: 'home-ada',
|
|
25
|
+
attrs: {
|
|
26
|
+
'nomos.owned.stage': 'awaitingRecoveryPrincipal',
|
|
27
|
+
'nomos.owned.orderId': 'NomosOwnedRecoveryOrder_1',
|
|
28
|
+
'nomos.owned.recoveryPrincipal': 'user:bob',
|
|
29
|
+
'nomos.owned.recommendedAction':
|
|
30
|
+
NomosRecommendedAction.openRecoveryPrincipalDevice,
|
|
31
|
+
},
|
|
32
|
+
));
|
|
33
|
+
expect(e, isA<OwnedRecoveryPending>());
|
|
34
|
+
final recovery = e as OwnedRecoveryPending;
|
|
35
|
+
expect(recovery.workspace, 'home-ada');
|
|
36
|
+
expect(recovery.stage, 'awaitingRecoveryPrincipal');
|
|
37
|
+
expect(recovery.orderId, 'NomosOwnedRecoveryOrder_1');
|
|
38
|
+
expect(recovery.recoveryPrincipal, 'user:bob');
|
|
39
|
+
expect(recovery.recommendedAction,
|
|
40
|
+
NomosRecommendedAction.openRecoveryPrincipalDevice);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test(
|
|
44
|
+
'law.* mints LawMismatchError with both sides + compat verdict + the exact message',
|
|
45
|
+
() {
|
|
12
46
|
final e = nomosErrorFromInfo(info(
|
|
13
47
|
'nomos.law.mismatch',
|
|
14
48
|
workspace: 'ws-1',
|
|
@@ -44,11 +78,15 @@ void main() {
|
|
|
44
78
|
'nomos.law.workspaceRuns': 'bbbb',
|
|
45
79
|
'nomos.law.structurallyCompatible': true,
|
|
46
80
|
})) as LawMismatchError;
|
|
47
|
-
expect(
|
|
81
|
+
expect(
|
|
82
|
+
m.mismatchMessage,
|
|
83
|
+
startsWith(
|
|
84
|
+
'client generated for law aaaa, workspace runs law bbbb; structurally compatible — proceeding —'));
|
|
48
85
|
});
|
|
49
86
|
|
|
50
87
|
test('auth.* mints AuthError with reauth as the remedy', () {
|
|
51
|
-
final e = nomosErrorFromInfo(info('nomos.auth.issuer_mismatch',
|
|
88
|
+
final e = nomosErrorFromInfo(info('nomos.auth.issuer_mismatch',
|
|
89
|
+
workspace: 'ws-1', attrs: {'nomos.auth.issuer': 'https://evil'}));
|
|
52
90
|
expect(e, isA<AuthError>());
|
|
53
91
|
final a = e as AuthError;
|
|
54
92
|
expect(a.code, 'nomos.auth.issuer_mismatch');
|
|
@@ -68,7 +106,9 @@ void main() {
|
|
|
68
106
|
expect(l.recommendedAction, NomosRecommendedAction.retry);
|
|
69
107
|
});
|
|
70
108
|
|
|
71
|
-
test(
|
|
109
|
+
test(
|
|
110
|
+
'authz signer_enrollment_missing mints ParentSignerEnrollmentMissing with the ONE remedy',
|
|
111
|
+
() {
|
|
72
112
|
final e = nomosErrorFromInfo(info(
|
|
73
113
|
'nomos.authz.signer_enrollment_missing',
|
|
74
114
|
workspace: 'co2-platform',
|
|
@@ -90,18 +130,26 @@ void main() {
|
|
|
90
130
|
expect(p.object, 'identity:u-1');
|
|
91
131
|
expect(p.relation, 'signer');
|
|
92
132
|
expect(p.directiveId, 'birthEstateWorkspace');
|
|
93
|
-
expect(p.recommendedAction,
|
|
133
|
+
expect(p.recommendedAction,
|
|
134
|
+
NomosRecommendedAction.ensureParentSignerEnrollment);
|
|
94
135
|
});
|
|
95
136
|
|
|
96
|
-
test(
|
|
137
|
+
test(
|
|
138
|
+
'authz cause=missing-signer-enrollment routes to ParentSignerEnrollmentMissing even under the generic code',
|
|
139
|
+
() {
|
|
97
140
|
final e = nomosErrorFromInfo(info(
|
|
98
141
|
'nomos.authz.refused',
|
|
99
|
-
attrs: {
|
|
142
|
+
attrs: {
|
|
143
|
+
'nomos.authz.cause': 'missing-signer-enrollment',
|
|
144
|
+
'nomos.authz.actor': 'user:u-9'
|
|
145
|
+
},
|
|
100
146
|
));
|
|
101
147
|
expect(e, isA<ParentSignerEnrollmentMissing>());
|
|
102
148
|
});
|
|
103
149
|
|
|
104
|
-
test(
|
|
150
|
+
test(
|
|
151
|
+
'authz missing_role stays AuthzRefusedError — role-absent is DISTINGUISHED from signer-absent',
|
|
152
|
+
() {
|
|
105
153
|
final e = nomosErrorFromInfo(info(
|
|
106
154
|
'nomos.authz.missing_role',
|
|
107
155
|
workspace: 'ws-1',
|
|
@@ -120,8 +168,11 @@ void main() {
|
|
|
120
168
|
expect(a.object, 'workspace:self');
|
|
121
169
|
});
|
|
122
170
|
|
|
123
|
-
test(
|
|
124
|
-
|
|
171
|
+
test(
|
|
172
|
+
'custody.* still mints CustodyError (unchanged), unknown codes stay generic',
|
|
173
|
+
() {
|
|
174
|
+
expect(nomosErrorFromInfo(info('nomos.custody.missing_object')),
|
|
175
|
+
isA<CustodyError>());
|
|
125
176
|
final g = nomosErrorFromInfo(info('nomos.bridge.error'));
|
|
126
177
|
expect(g, isA<NomosBridgeError>());
|
|
127
178
|
expect(g, isNot(isA<LawMismatchError>()));
|