@githolon/dsl 0.100.6 → 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 +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- 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 +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- 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 +155 -45
- 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 +47 -2
- 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
|
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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>()));
|
|
@@ -115,6 +115,9 @@ void main() {
|
|
|
115
115
|
// keyed 3b legs all log enter AND exit lines.
|
|
116
116
|
expect(diagnostics.any((l) => l.contains('offline law install')), isTrue,
|
|
117
117
|
reason: 'phase 1 install diagnostics must appear');
|
|
118
|
+
expect(diagnostics.any((l) => l.startsWith('[offline-install]')), isTrue,
|
|
119
|
+
reason:
|
|
120
|
+
'an in-repo runner must install through its adjacent in-repo helper, never silently fall back to the published CLI');
|
|
118
121
|
expect(
|
|
119
122
|
diagnostics
|
|
120
123
|
.any((l) => l.contains('runner ready') && l.contains('done')),
|
|
@@ -134,11 +137,11 @@ void main() {
|
|
|
134
137
|
});
|
|
135
138
|
|
|
136
139
|
try {
|
|
137
|
-
// ── LEG 2: the blessed session-bound shape —
|
|
138
|
-
//
|
|
140
|
+
// ── LEG 2: the blessed session-bound shape — one business birth, then Estate → Site → Asset
|
|
141
|
+
// through the generated Dart client. Zero routing/security strings. ──
|
|
139
142
|
final home = harness.session('home');
|
|
140
143
|
final bornEstate = await _leg(
|
|
141
|
-
'leg 2:
|
|
144
|
+
'leg 2: generated business shape (Estate → Site → Asset)',
|
|
142
145
|
() async {
|
|
143
146
|
final homeClient = await Co2Client.bind(home, actor: _owner);
|
|
144
147
|
expect(homeClient.session!.workspace, home.workspace,
|
|
@@ -166,6 +169,29 @@ void main() {
|
|
|
166
169
|
reason: 'the born business root reads back typed');
|
|
167
170
|
expect(rows.single.reference, 'E-001');
|
|
168
171
|
expect(rows.single.name, 'Foundry');
|
|
172
|
+
|
|
173
|
+
final siteOutcome = await estateClient.addSite(
|
|
174
|
+
AddSitePayload(estateId: ref.id, name: 'Trafford Depot'),
|
|
175
|
+
);
|
|
176
|
+
final siteRef = siteOutcome.created.single.ref;
|
|
177
|
+
expect(siteRef.type, 'co2.Site');
|
|
178
|
+
final sites = await estateClient.sitesByEstate(estateId: ref.id);
|
|
179
|
+
expect(sites, hasLength(1));
|
|
180
|
+
expect(sites.single.name, 'Trafford Depot');
|
|
181
|
+
|
|
182
|
+
final assetOutcome = await estateClient.addAsset(
|
|
183
|
+
AddAssetPayload(
|
|
184
|
+
siteId: siteRef.id,
|
|
185
|
+
name: 'Main electricity meter',
|
|
186
|
+
operationalStatus: 'commissioned',
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
final assetRef = assetOutcome.created.single.ref;
|
|
190
|
+
expect(assetRef.type, 'co2.Asset');
|
|
191
|
+
final assets = await estateClient.assetsBySite(siteId: siteRef.id);
|
|
192
|
+
expect(assets, hasLength(1));
|
|
193
|
+
expect(assets.single.name, 'Main electricity meter');
|
|
194
|
+
expect(assets.single.operationalStatus?.valueOrNull, 'commissioned');
|
|
169
195
|
return (session: estateSession, ref: ref);
|
|
170
196
|
});
|
|
171
197
|
final estate = bornEstate.session;
|