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