@githolon/dsl 0.72.1 → 0.73.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
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 0.49.0
|
|
2
|
+
|
|
3
|
+
- **Fix: local-only `autoSync()` deadlocked the multi-workspace plane.** A `local://` session's
|
|
4
|
+
autoSync registered the full cloud machinery (doorbell WebSocket retry loop, fallback timers,
|
|
5
|
+
intent-offer POSTs that could never land) — the future never completed, the runner dispatcher
|
|
6
|
+
starved, sibling queries and `listWorkspaces()` hung, and teardown wedged. `autoSync()` on a
|
|
7
|
+
local-only session now registers nothing and returns immediately; `sync()` is a true no-op
|
|
8
|
+
(`localOnly: true` summary); a local:// holon performs ZERO network attempts. (co2's acceptance
|
|
9
|
+
test implemented verbatim; failed before, passes after.)
|
|
10
|
+
- **New: `acquireSession(workspace, {ifAbsentCredentials})`** — atomic single-flight session
|
|
11
|
+
acquisition: reuses a connected session, joins an in-flight open (concurrent calls yield the
|
|
12
|
+
same handle, one connect), opens local-only for workspaces the plane already knows
|
|
13
|
+
(mounted/parked/resident custody — credentials never invoked), and only calls
|
|
14
|
+
`ifAbsentCredentials` (exactly once) when a genuine cloud open is required.
|
|
15
|
+
|
|
1
16
|
## 0.48.1
|
|
2
17
|
|
|
3
18
|
- **Fix: `NomosSession.watchPendingConflicts()` never emitted.** The session overrode every other
|
|
@@ -661,6 +661,34 @@ class NomosOfferOutcome {
|
|
|
661
661
|
.toList();
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
+
/// What a GENUINE cloud open needs — returned by an [NomosBridge.acquireSession] `ifAbsentCredentials`
|
|
665
|
+
/// callback, invoked ONLY when the workspace is nowhere on the local plane (never for a local-only or
|
|
666
|
+
/// already-mounted workspace). Mirrors the cloud-shaped subset of [NomosBridge.connect]'s parameters.
|
|
667
|
+
class NomosSessionCredentials {
|
|
668
|
+
/// The cloud base URL the workspace syncs from (e.g. `https://nomos.captainapp.co.uk`).
|
|
669
|
+
final String cloud;
|
|
670
|
+
|
|
671
|
+
/// The verified principal's access token (`x-nomos-auth`), when the workspace requires one.
|
|
672
|
+
final String? authToken;
|
|
673
|
+
|
|
674
|
+
/// The client id for this device's replica (defaults per the runner when omitted).
|
|
675
|
+
final String? clientId;
|
|
676
|
+
|
|
677
|
+
/// The device signing secret (base64, from `authorKeygen`) — the warrant default for every offer.
|
|
678
|
+
final String? authorSecret;
|
|
679
|
+
|
|
680
|
+
/// Open even when the workspace has no remote main yet (an unborn workspace reads empty).
|
|
681
|
+
final bool allowUnborn;
|
|
682
|
+
|
|
683
|
+
const NomosSessionCredentials({
|
|
684
|
+
required this.cloud,
|
|
685
|
+
this.authToken,
|
|
686
|
+
this.clientId,
|
|
687
|
+
this.authorSecret,
|
|
688
|
+
this.allowUnborn = false,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
664
692
|
/// THE BRIDGE. Construct over a [BridgeTransport], `await ready`, then `connect(...)` and drive the holon.
|
|
665
693
|
class NomosBridge {
|
|
666
694
|
final BridgeTransport _transport;
|
|
@@ -708,6 +736,9 @@ class NomosBridge {
|
|
|
708
736
|
// caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
|
|
709
737
|
final Set<String> _sessionNames = {};
|
|
710
738
|
final Map<String, NomosSession> _sessionViews = {};
|
|
739
|
+
// [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
|
|
740
|
+
// acquirer joins (removed when it completes; a live session then hits the fast path instead).
|
|
741
|
+
final Map<String, Future<NomosSession>> _acquiring = {};
|
|
711
742
|
int _seq = 0;
|
|
712
743
|
final String _traceId = _newTraceId();
|
|
713
744
|
late final StreamSubscription<Map<String, Object?>> _sub;
|
|
@@ -1205,6 +1236,60 @@ class NomosBridge {
|
|
|
1205
1236
|
return session(ws);
|
|
1206
1237
|
}
|
|
1207
1238
|
|
|
1239
|
+
/// ACQUIRE A SESSION ATOMICALLY — the plug-and-play multi-workspace open (co2 DX ask). Semantics:
|
|
1240
|
+
/// • an already-connected session is returned as-is (no work, no credentials);
|
|
1241
|
+
/// • a workspace the local plane already holds (mounted by another path, LRU-parked, or with resident
|
|
1242
|
+
/// custody — e.g. a locally-born child) is opened LOCAL-ONLY, in place (no cloud, no credentials);
|
|
1243
|
+
/// • two concurrent [acquireSession] calls for the same workspace SINGLE-FLIGHT: the second joins the
|
|
1244
|
+
/// first's in-flight open and both yield the SAME [NomosSession] — one connect, never two;
|
|
1245
|
+
/// • ONLY when a genuine cloud open is required (the workspace is nowhere on this device) is
|
|
1246
|
+
/// [ifAbsentCredentials] invoked — exactly once — and the connect made with what it returns.
|
|
1247
|
+
/// Omitting [ifAbsentCredentials] makes a genuinely-absent workspace a named [StateError] instead of a
|
|
1248
|
+
/// silent cloudless mount.
|
|
1249
|
+
Future<NomosSession> acquireSession(String workspace,
|
|
1250
|
+
{Future<NomosSessionCredentials> Function()? ifAbsentCredentials}) {
|
|
1251
|
+
// Fast path: a live session needs no runner round-trip (and never asks for credentials).
|
|
1252
|
+
if (_sessionNames.contains(workspace)) return Future.value(session(workspace));
|
|
1253
|
+
// SINGLE-FLIGHT: one in-flight open per workspace per plane; concurrent callers join it.
|
|
1254
|
+
return _acquiring.putIfAbsent(workspace, () async {
|
|
1255
|
+
try {
|
|
1256
|
+
if (_sessionNames.contains(workspace)) return session(workspace);
|
|
1257
|
+
// Consult the plane's TRUTHFUL registry (mounted ∪ parked ∪ resident custody — the runner's
|
|
1258
|
+
// realm.list()): anything the device already holds opens locally, no cloud, no credentials.
|
|
1259
|
+
var resident = false;
|
|
1260
|
+
try {
|
|
1261
|
+
resident = (await listWorkspaces())
|
|
1262
|
+
.any((w) => w['workspace']?.toString() == workspace);
|
|
1263
|
+
} catch (_) {
|
|
1264
|
+
// a pre-connect plane has no registry yet — fall through to the credentials decision below.
|
|
1265
|
+
}
|
|
1266
|
+
if (resident) {
|
|
1267
|
+
await connect(workspace: workspace, session: workspace, allowUnborn: true);
|
|
1268
|
+
return session(workspace);
|
|
1269
|
+
}
|
|
1270
|
+
if (ifAbsentCredentials == null) {
|
|
1271
|
+
throw StateError(
|
|
1272
|
+
'acquireSession("$workspace"): the workspace is not on this device\'s plane and no '
|
|
1273
|
+
'ifAbsentCredentials was given — a cloud open is genuinely required');
|
|
1274
|
+
}
|
|
1275
|
+
final c = await ifAbsentCredentials();
|
|
1276
|
+
await connect(
|
|
1277
|
+
cloud: c.cloud,
|
|
1278
|
+
workspace: workspace,
|
|
1279
|
+
session: workspace,
|
|
1280
|
+
clientId: c.clientId,
|
|
1281
|
+
authToken: c.authToken,
|
|
1282
|
+
authorSecret: c.authorSecret,
|
|
1283
|
+
allowUnborn: c.allowUnborn,
|
|
1284
|
+
);
|
|
1285
|
+
return session(workspace);
|
|
1286
|
+
} finally {
|
|
1287
|
+
// completed (either way) — the next acquire re-evaluates fresh (a live session hits the fast path).
|
|
1288
|
+
_acquiring.remove(workspace);
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1208
1293
|
/// Reserve a KERNEL-MINTED typed id (`<TypeTag>_<uuidv7>`) via the holon's front-door mint — for app-side
|
|
1209
1294
|
/// references (routing keys, optimistic UI). `.creates` payloads still carry NO id: the LAW mints those
|
|
1210
1295
|
/// (see [offerCreates], which returns them).
|
|
@@ -1947,6 +2032,13 @@ class NomosSession extends NomosBridge {
|
|
|
1947
2032
|
@override
|
|
1948
2033
|
NomosSession session(String workspace) => parent.session(workspace);
|
|
1949
2034
|
|
|
2035
|
+
/// An acquired session mounts on the SHARED plane — delegate to the parent (the plane owns the
|
|
2036
|
+
/// single-flight registry; a session never owns mounts).
|
|
2037
|
+
@override
|
|
2038
|
+
Future<NomosSession> acquireSession(String workspace,
|
|
2039
|
+
{Future<NomosSessionCredentials> Function()? ifAbsentCredentials}) =>
|
|
2040
|
+
parent.acquireSession(workspace, ifAbsentCredentials: ifAbsentCredentials);
|
|
2041
|
+
|
|
1950
2042
|
/// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
|
|
1951
2043
|
@override
|
|
1952
2044
|
Future<NomosSession> follow(NomosRef ref,
|
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.49.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// acquireSession — the ATOMIC session-open (co2 DX ask): reuses a mounted session, opens a plane-resident
|
|
2
|
+
// (parked / resident-custody) workspace locally, JOINS an in-flight open (single-flight per workspace), and
|
|
3
|
+
// invokes the credentials callback ONLY when a cloud open is genuinely required. Proven hermetically over a
|
|
4
|
+
// fake transport (the same style as webview_bridge_test.dart) — the live-runner leg rides
|
|
5
|
+
// cluster_local_autosync_test.dart.
|
|
6
|
+
import 'dart:async';
|
|
7
|
+
|
|
8
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
9
|
+
import 'package:test/test.dart';
|
|
10
|
+
|
|
11
|
+
/// A fake runner: answers connect (after a real async gap, so concurrency is observable) and
|
|
12
|
+
/// listWorkspaces (from [resident]); records every request.
|
|
13
|
+
class FakeRunner implements BridgeTransport {
|
|
14
|
+
final _incoming = StreamController<Map<String, Object?>>.broadcast();
|
|
15
|
+
final List<Map<String, Object?>> sent = [];
|
|
16
|
+
final List<String> resident; // workspaces the plane already holds (realm.list())
|
|
17
|
+
|
|
18
|
+
FakeRunner({this.resident = const []}) {
|
|
19
|
+
scheduleMicrotask(
|
|
20
|
+
() => _incoming.add({'v': kWebviewBridgeVersion, 'event': 'ready'}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
int get connectCount => sent.where((m) => m['op'] == 'connect').length;
|
|
24
|
+
Map<String, Object?> connectArgs(int i) =>
|
|
25
|
+
(sent.where((m) => m['op'] == 'connect').elementAt(i)['args'] as Map)
|
|
26
|
+
.cast<String, Object?>();
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
void send(Map<String, Object?> message) {
|
|
33
|
+
sent.add(message);
|
|
34
|
+
final id = message['id'] as int;
|
|
35
|
+
final op = message['op'] as String;
|
|
36
|
+
final args = (message['args'] as Map).cast<String, Object?>();
|
|
37
|
+
Future<void>.delayed(const Duration(milliseconds: 5), () {
|
|
38
|
+
Object? result;
|
|
39
|
+
switch (op) {
|
|
40
|
+
case 'connect':
|
|
41
|
+
result = {'workspace': args['workspace']};
|
|
42
|
+
break;
|
|
43
|
+
case 'listWorkspaces':
|
|
44
|
+
result = {
|
|
45
|
+
'workspaces': [
|
|
46
|
+
for (final ws in resident)
|
|
47
|
+
{
|
|
48
|
+
'session': ws,
|
|
49
|
+
'workspace': ws,
|
|
50
|
+
'mounted': false,
|
|
51
|
+
'parked': true,
|
|
52
|
+
'syncPosture': 'local-only'
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
result = {};
|
|
59
|
+
}
|
|
60
|
+
_incoming
|
|
61
|
+
.add({'v': kWebviewBridgeVersion, 'id': id, 'ok': true, 'result': result});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void main() {
|
|
67
|
+
test('an already-connected session is reused — credentials NEVER invoked, no second connect', () async {
|
|
68
|
+
final runner = FakeRunner();
|
|
69
|
+
final bridge = NomosBridge(runner);
|
|
70
|
+
await bridge.ready;
|
|
71
|
+
await bridge.connect(workspace: 'home', session: 'home');
|
|
72
|
+
expect(runner.connectCount, 1);
|
|
73
|
+
|
|
74
|
+
final s = await bridge.acquireSession('home',
|
|
75
|
+
ifAbsentCredentials: () async =>
|
|
76
|
+
fail('credentials must not be requested for a mounted session'));
|
|
77
|
+
expect(identical(s, bridge.session('home')), isTrue);
|
|
78
|
+
expect(runner.connectCount, 1, reason: 'no re-connect for a live session');
|
|
79
|
+
await bridge.dispose();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('a plane-resident (parked) workspace opens LOCALLY — no cloud, credentials NOT invoked', () async {
|
|
83
|
+
final runner = FakeRunner(resident: ['estate']);
|
|
84
|
+
final bridge = NomosBridge(runner);
|
|
85
|
+
await bridge.ready;
|
|
86
|
+
|
|
87
|
+
var asked = false;
|
|
88
|
+
final s = await bridge.acquireSession('estate', ifAbsentCredentials: () async {
|
|
89
|
+
asked = true;
|
|
90
|
+
return const NomosSessionCredentials(cloud: 'https://never.invalid');
|
|
91
|
+
});
|
|
92
|
+
expect(asked, isFalse, reason: 'a resident workspace never asks for credentials');
|
|
93
|
+
expect(s.workspace, 'estate');
|
|
94
|
+
expect(runner.connectCount, 1);
|
|
95
|
+
expect(runner.connectArgs(0).containsKey('cloud'), isFalse,
|
|
96
|
+
reason: 'a resident open is local-only — no cloud in the connect');
|
|
97
|
+
await bridge.dispose();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('concurrent acquisition SINGLE-FLIGHTS: one open, one credentials call, the same session', () async {
|
|
101
|
+
final runner = FakeRunner();
|
|
102
|
+
final bridge = NomosBridge(runner);
|
|
103
|
+
await bridge.ready;
|
|
104
|
+
|
|
105
|
+
var credentialCalls = 0;
|
|
106
|
+
Future<NomosSessionCredentials> creds() async {
|
|
107
|
+
credentialCalls++;
|
|
108
|
+
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
109
|
+
return const NomosSessionCredentials(
|
|
110
|
+
cloud: 'https://cloud.example', authToken: 'tok', allowUnborn: true);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
final results = await Future.wait([
|
|
114
|
+
bridge.acquireSession('cloudy', ifAbsentCredentials: creds),
|
|
115
|
+
bridge.acquireSession('cloudy', ifAbsentCredentials: creds),
|
|
116
|
+
bridge.acquireSession('cloudy', ifAbsentCredentials: creds),
|
|
117
|
+
]);
|
|
118
|
+
expect(credentialCalls, 1, reason: 'the credentials callback fires exactly once');
|
|
119
|
+
expect(runner.connectCount, 1, reason: 'ONE connect for three concurrent acquires');
|
|
120
|
+
expect(identical(results[0], results[1]), isTrue);
|
|
121
|
+
expect(identical(results[1], results[2]), isTrue);
|
|
122
|
+
final args = runner.connectArgs(0);
|
|
123
|
+
expect(args['cloud'], 'https://cloud.example');
|
|
124
|
+
expect(args['authToken'], 'tok');
|
|
125
|
+
expect(args['allowUnborn'], isTrue);
|
|
126
|
+
await bridge.dispose();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('genuinely absent + no credentials callback → a named StateError (never a silent cloudless mount)',
|
|
130
|
+
() async {
|
|
131
|
+
final runner = FakeRunner();
|
|
132
|
+
final bridge = NomosBridge(runner);
|
|
133
|
+
await bridge.ready;
|
|
134
|
+
await expectLater(bridge.acquireSession('nowhere'), throwsStateError);
|
|
135
|
+
expect(runner.connectCount, 0);
|
|
136
|
+
await bridge.dispose();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('a NomosSession delegates acquireSession to the parent plane (one registry)', () async {
|
|
140
|
+
final runner = FakeRunner();
|
|
141
|
+
final bridge = NomosBridge(runner);
|
|
142
|
+
await bridge.ready;
|
|
143
|
+
await bridge.connect(workspace: 'home', session: 'home');
|
|
144
|
+
await bridge.connect(workspace: 'estate', session: 'estate');
|
|
145
|
+
|
|
146
|
+
final viaSession = await bridge.session('home').acquireSession('estate',
|
|
147
|
+
ifAbsentCredentials: () async =>
|
|
148
|
+
fail('estate is already mounted — no credentials'));
|
|
149
|
+
expect(identical(viaSession, bridge.session('estate')), isTrue);
|
|
150
|
+
await bridge.dispose();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// CO2 ACCEPTANCE — local-only autoSync must not block the multi-workspace plane (their report, verbatim):
|
|
2
|
+
// - Open multiple local-only sessions with NomosTestHarness.cluster.
|
|
3
|
+
// - await homeSession.autoSync() completes promptly.
|
|
4
|
+
// - await bridge.listWorkspaces() and a query on a sibling estate session still complete.
|
|
5
|
+
// - stopAutoSync() and harness disposal complete.
|
|
6
|
+
// - No local://.../intent-offers fetch attempts occur.
|
|
7
|
+
// autoSync() REGISTERS background behavior and returns immediately; for a local-only (local://) session it
|
|
8
|
+
// performs NO network work and must not monopolize the runner request dispatcher.
|
|
9
|
+
@TestOn('vm')
|
|
10
|
+
library;
|
|
11
|
+
|
|
12
|
+
import 'dart:io';
|
|
13
|
+
|
|
14
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
15
|
+
import 'package:test/test.dart';
|
|
16
|
+
|
|
17
|
+
String get _root => '${Directory.current.path}/../..';
|
|
18
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
19
|
+
String get _guestbookDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
|
|
20
|
+
String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
21
|
+
|
|
22
|
+
String? _missingPrereq() {
|
|
23
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
24
|
+
if (!File(_guestbookDeploy).existsSync()) {
|
|
25
|
+
return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
|
|
26
|
+
}
|
|
27
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
28
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
29
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
void main() {
|
|
35
|
+
test(
|
|
36
|
+
'local-only autoSync() returns promptly, never fetches, and does not block the plane (co2 acceptance)',
|
|
37
|
+
() async {
|
|
38
|
+
final skip = _missingPrereq();
|
|
39
|
+
if (skip != null) {
|
|
40
|
+
markTestSkipped(skip);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 1. Open multiple local-only sessions with NomosTestHarness.cluster (four, one bridge).
|
|
45
|
+
final names = ['home', 'estate-a', 'estate-b', 'estate-c'];
|
|
46
|
+
final diagnostics = <String>[];
|
|
47
|
+
final harness = await NomosTestHarness.cluster(
|
|
48
|
+
workspaces: {for (final ws in names) ws: [_guestbookDeploy]},
|
|
49
|
+
nodeEntry: _repoEntry,
|
|
50
|
+
runtimeDir: _runtimeDir,
|
|
51
|
+
onDiagnostic: diagnostics.add,
|
|
52
|
+
);
|
|
53
|
+
try {
|
|
54
|
+
final home = harness.session('home');
|
|
55
|
+
|
|
56
|
+
// A realistic home holds local writes (ahead of any base) — this is what drives the up-leg machinery.
|
|
57
|
+
await home.offerDirective(
|
|
58
|
+
domain: 'guestbook',
|
|
59
|
+
directiveId: 'signGuestbook',
|
|
60
|
+
payload: {
|
|
61
|
+
'author': 'ada',
|
|
62
|
+
'message': 'offline write before autoSync',
|
|
63
|
+
'mood': 'delighted',
|
|
64
|
+
'signedAt': '2026-07-11T00:00:00Z',
|
|
65
|
+
},
|
|
66
|
+
actor: 'ada',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// 2. await homeSession.autoSync() completes promptly.
|
|
70
|
+
await home.autoSync().timeout(const Duration(seconds: 10),
|
|
71
|
+
onTimeout: () => throw StateError(
|
|
72
|
+
'autoSync() on a local-only session did not complete within 10s (the reported deadlock)'));
|
|
73
|
+
|
|
74
|
+
// 3. await bridge.listWorkspaces() and a query on a sibling estate session still complete.
|
|
75
|
+
final listed = await harness.bridge.listWorkspaces().timeout(
|
|
76
|
+
const Duration(seconds: 10),
|
|
77
|
+
onTimeout: () => throw StateError(
|
|
78
|
+
'listWorkspaces() hung after a local-only autoSync (dispatcher monopolized)'));
|
|
79
|
+
expect(listed.map((w) => w['session'].toString()).toSet(),
|
|
80
|
+
containsAll(names.toSet()));
|
|
81
|
+
final sibling = await harness
|
|
82
|
+
.session('estate-a')
|
|
83
|
+
.query('entriesByAuthor', {'author': 'nobody'}).timeout(
|
|
84
|
+
const Duration(seconds: 10),
|
|
85
|
+
onTimeout: () => throw StateError(
|
|
86
|
+
'a sibling estate query hung after a local-only autoSync'));
|
|
87
|
+
expect(sibling, isEmpty);
|
|
88
|
+
|
|
89
|
+
// Give any wrongly-registered background sync machinery time to fire so its fetch attempts would show.
|
|
90
|
+
await Future<void>.delayed(const Duration(seconds: 2));
|
|
91
|
+
|
|
92
|
+
// 4. stopAutoSync() and harness disposal complete (disposal asserted in finally / teardown).
|
|
93
|
+
await home.stopAutoSync().timeout(const Duration(seconds: 10),
|
|
94
|
+
onTimeout: () =>
|
|
95
|
+
throw StateError('stopAutoSync() hung on a local-only session'));
|
|
96
|
+
|
|
97
|
+
// Bonus (live-runner leg of the acquireSession DX ask): a mounted local workspace is acquired
|
|
98
|
+
// WITHOUT credentials, and yields the same cached session handle.
|
|
99
|
+
final acquired = await harness.bridge.acquireSession('estate-b',
|
|
100
|
+
ifAbsentCredentials: () async =>
|
|
101
|
+
fail('estate-b is mounted on the plane — credentials must not be requested'));
|
|
102
|
+
expect(identical(acquired, harness.session('estate-b')), isTrue);
|
|
103
|
+
|
|
104
|
+
// 5. No local://.../intent-offers fetch attempts occur (attempted-and-refused still counts as attempted).
|
|
105
|
+
final offending = diagnostics
|
|
106
|
+
.where((l) => l.contains('intent-offers') || l.contains('refusing network fetch'))
|
|
107
|
+
.toList();
|
|
108
|
+
expect(offending, isEmpty,
|
|
109
|
+
reason:
|
|
110
|
+
'a local-only session must perform NO network work — saw:\n${offending.join('\n')}');
|
|
111
|
+
} finally {
|
|
112
|
+
await harness
|
|
113
|
+
.dispose()
|
|
114
|
+
.timeout(const Duration(seconds: 20), onTimeout: () {
|
|
115
|
+
throw StateError('harness disposal hung (the reported teardown hang)');
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -904,7 +904,7 @@ const NOMOS_TYPES_DEP = "^0.3.0";
|
|
|
904
904
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
905
905
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
906
906
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
907
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
907
|
+
const NOMOS_CLIENT_DEP = "^0.49.0";
|
|
908
908
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
909
909
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
910
910
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|