@githolon/dsl 0.46.0 → 0.48.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/lib/src/errors.dart +250 -0
- package/dart/lib/src/test_harness.dart +182 -175
- package/dart/test/errors_test.dart +77 -0
- package/dart/test/test_harness_test.dart +85 -57
- package/package.json +1 -1
- package/src/codegen_dart_client.ts +15 -0
- package/src/codegen_proof.ts +123 -8
- package/src/codegen_ts.ts +28 -3
- package/src/compat_manifest.ts +20 -0
- package/src/compile_package_main.ts +174 -16
- package/src/framework/framework_aggregates.ts +18 -0
- package/src/read_projection_composition.ts +9 -9
- package/src/read_projection_namespace.ts +4 -5
- package/src/usd.ts +3 -3
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
// NOMOS TEST HARNESS —
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// NOMOS TEST HARNESS — an EPHEMERAL, FULLY-OFFLINE real-law fixture for headless app tests. One call
|
|
2
|
+
// installs a tenant's compiled law into a FRESH LOCAL workspace (no cloud contact, ever), boots the REAL
|
|
3
|
+
// githolon (the byte-identical wasm, the real ledger, the real gate) over [NodeRunnerTransport], and hands
|
|
4
|
+
// out a [NomosBridge] the generated Dart clients drive — the SAME surface the app UI uses — under
|
|
5
|
+
// `flutter test` / `dart test` / CI.
|
|
5
6
|
//
|
|
6
7
|
// ```dart
|
|
7
8
|
// final harness = await NomosTestHarness.ephemeral(
|
|
8
9
|
// deployJsonPaths: ['build/guestbook.deploy.json'],
|
|
10
|
+
// registerTearDown: addTearDown, // auto-disposes when the test ends (see below)
|
|
9
11
|
// );
|
|
10
|
-
// final
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// await harness.dispose();
|
|
12
|
+
// final app = guestbookClient(harness.bridge); // the generated client — same one the app uses
|
|
13
|
+
// await app.signGuestbook(author: 'ada', message: 'hi', mood: 'happy', signedAt: ...);
|
|
14
|
+
// final rows = await app.entriesByAuthor(author: 'ada');
|
|
15
|
+
// await harness.dispose(); // DETERMINISTIC teardown (also auto-fired via registerTearDown)
|
|
14
16
|
// ```
|
|
15
17
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// HOW IT STAYS OFFLINE. Two node children, both talking only to the local disk:
|
|
19
|
+
// 1. `offline-install.mjs` runs the PROVEN offline install lane (`githolon proof`'s adapter engine) —
|
|
20
|
+
// genesis → framework controller → each tenant package's `installDomain`, folded through the ONE gate —
|
|
21
|
+
// and serializes the workspace tree to a snapshot in a fresh temp dir.
|
|
22
|
+
// 2. the node runner (`node-entry.mjs`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
|
|
23
|
+
// local cache (`~/.holon/runtime`, filled by any warm `githolon` run) and boots the holon from that
|
|
24
|
+
// snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
|
|
21
25
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
+
// PREREQUISITES (documented so a failure is legible): `node` on PATH; a resolvable
|
|
27
|
+
// `@githolon/client/runner/node-entry.mjs`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
|
|
28
|
+
// one `githolon` run fills it). All local; no network at test time.
|
|
29
|
+
//
|
|
30
|
+
// PACKAGING NOTE: `offline-install.mjs` imports the adapter engine, which today lives beside the in-repo
|
|
31
|
+
// runner. For a PUBLISHED harness this install belongs behind a `githolon` subcommand (the CLI already
|
|
32
|
+
// bundles the engine + this exact sequence in `proof_offline`); [installHelper] is the one seam to repoint.
|
|
26
33
|
import 'dart:convert';
|
|
27
34
|
import 'dart:io';
|
|
28
|
-
import 'dart:math';
|
|
29
35
|
|
|
30
36
|
import 'node_runner_transport.dart';
|
|
31
37
|
import 'webview_bridge.dart' show NomosBridge;
|
|
@@ -38,198 +44,199 @@ class NomosTestHarnessException implements Exception {
|
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
class NomosTestHarness {
|
|
41
|
-
/// The
|
|
42
|
-
final
|
|
47
|
+
/// The connected bridge — hand it to the generated typed clients (the same surface the app UI uses).
|
|
48
|
+
final NomosBridge bridge;
|
|
43
49
|
|
|
44
|
-
/// The
|
|
50
|
+
/// The local workspace name the law was installed into.
|
|
45
51
|
final String workspace;
|
|
46
52
|
|
|
47
|
-
final
|
|
48
|
-
final
|
|
49
|
-
final String? _nodeEntry;
|
|
50
|
-
final String? _resolveFrom;
|
|
53
|
+
final NodeRunnerTransport _transport;
|
|
54
|
+
final Directory _workdir;
|
|
51
55
|
final void Function(String line)? _onDiagnostic;
|
|
52
|
-
final List<NodeRunnerTransport> _transports = [];
|
|
53
56
|
bool _disposed = false;
|
|
54
57
|
|
|
55
|
-
NomosTestHarness._(this.
|
|
56
|
-
this.
|
|
58
|
+
NomosTestHarness._(this.bridge, this.workspace, this._transport, this._workdir,
|
|
59
|
+
this._onDiagnostic);
|
|
57
60
|
|
|
58
|
-
///
|
|
61
|
+
/// Install [deployJsonPaths] into a fresh LOCAL workspace and boot a bridge over it — NO cloud contact.
|
|
59
62
|
///
|
|
60
|
-
/// [deployJsonPaths] — the `build/<name>.deploy.json` files `nomos-compile` emits (
|
|
61
|
-
/// platform
|
|
62
|
-
/// [workspace] —
|
|
63
|
-
/// [
|
|
64
|
-
///
|
|
65
|
-
///
|
|
63
|
+
/// [deployJsonPaths] — the `build/<name>.deploy.json` files `nomos-compile` emits (installed in order;
|
|
64
|
+
/// framework/platform law before a domain that depends on it).
|
|
65
|
+
/// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
|
|
66
|
+
/// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` + `packages.json`.
|
|
67
|
+
/// [nodeEntry] — path to `node-entry.mjs`; when omitted it is resolved from [resolveFrom] via
|
|
68
|
+
/// `require.resolve('@githolon/client/runner/node-entry.mjs')`.
|
|
69
|
+
/// [installHelper] — path to `offline-install.mjs`; defaults to the file beside [nodeEntry].
|
|
70
|
+
/// [registerTearDown] — pass the test framework's `addTearDown` to auto-[dispose] when the test ends
|
|
71
|
+
/// (feature-detecting `package:test` from a library is not clean in Dart, so this is explicit and safe).
|
|
72
|
+
/// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
|
|
66
73
|
static Future<NomosTestHarness> ephemeral({
|
|
67
|
-
List<String> deployJsonPaths
|
|
68
|
-
String
|
|
69
|
-
String
|
|
70
|
-
String?
|
|
71
|
-
String? authToken,
|
|
74
|
+
required List<String> deployJsonPaths,
|
|
75
|
+
String workspace = 'test-ws',
|
|
76
|
+
String clientId = 'test-runner',
|
|
77
|
+
String? runtimeDir,
|
|
72
78
|
String? nodeEntry,
|
|
79
|
+
String? installHelper,
|
|
73
80
|
String? resolveFrom,
|
|
81
|
+
String node = 'node',
|
|
82
|
+
bool autoTearDown = true,
|
|
83
|
+
void Function(Future<void> Function())? registerTearDown,
|
|
74
84
|
void Function(String line)? onDiagnostic,
|
|
85
|
+
Duration readyTimeout = const Duration(seconds: 30),
|
|
75
86
|
}) async {
|
|
76
|
-
|
|
77
|
-
final ws = workspace ?? 'test-${_hex(12)}';
|
|
78
|
-
final harness = NomosTestHarness._(_stripSlash(cloud), ws, id.principal,
|
|
79
|
-
id.authToken, nodeEntry, resolveFrom, onDiagnostic);
|
|
80
|
-
await harness._create();
|
|
81
|
-
for (final p in deployJsonPaths) {
|
|
82
|
-
await harness.deployLaw(p);
|
|
83
|
-
}
|
|
84
|
-
return harness;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
static String _brief(String s) => s.length > 600 ? '${s.substring(0, 600)}…[+${s.length - 600} chars]' : s;
|
|
88
|
-
|
|
89
|
-
/// Deploy one more compiled package (`<name>.deploy.json`) into the workspace — e.g. a law evolve mid-test.
|
|
90
|
-
Future<void> deployLaw(String deployJsonPath) async {
|
|
91
|
-
final f = File(deployJsonPath);
|
|
92
|
-
if (!f.existsSync()) {
|
|
87
|
+
if (deployJsonPaths.isEmpty) {
|
|
93
88
|
throw NomosTestHarnessException(
|
|
94
|
-
'
|
|
89
|
+
'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits');
|
|
95
90
|
}
|
|
96
|
-
final
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
final entry = nodeEntry ??
|
|
92
|
+
await _resolveEntry(node, resolveFrom ?? Directory.current.path);
|
|
93
|
+
final helper = installHelper ??
|
|
94
|
+
'${File(entry).parent.path}/offline-install.mjs';
|
|
95
|
+
|
|
96
|
+
// A FRESH temp dir per harness — the snapshot + any transient runner state live here and nowhere else,
|
|
97
|
+
// so dispose() leaves nothing behind.
|
|
98
|
+
final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
|
|
99
|
+
// Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
|
|
100
|
+
NodeRunnerTransport? transport;
|
|
101
|
+
try {
|
|
102
|
+
final snapshot = '${workdir.path}/snapshot.bin';
|
|
103
|
+
await _runInstall(
|
|
104
|
+
node: node,
|
|
105
|
+
helper: helper,
|
|
106
|
+
deployJsonPaths: deployJsonPaths,
|
|
107
|
+
workspace: workspace,
|
|
108
|
+
snapshot: snapshot,
|
|
109
|
+
runtimeDir: runtimeDir,
|
|
110
|
+
onDiagnostic: onDiagnostic);
|
|
111
|
+
|
|
112
|
+
transport = await NodeRunnerTransport.start(
|
|
113
|
+
entry: entry,
|
|
114
|
+
resolveFrom: resolveFrom,
|
|
115
|
+
node: node,
|
|
116
|
+
environment: {
|
|
117
|
+
'NOMOS_OFFLINE': '1',
|
|
118
|
+
'NOMOS_OFFLINE_RESTORE': snapshot,
|
|
119
|
+
if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
|
|
120
|
+
},
|
|
121
|
+
onDiagnostic: onDiagnostic,
|
|
122
|
+
);
|
|
123
|
+
final bridge = NomosBridge(transport);
|
|
124
|
+
await bridge.ready.timeout(readyTimeout,
|
|
125
|
+
onTimeout: () => throw NomosTestHarnessException(
|
|
126
|
+
'the offline runner never signalled ready within $readyTimeout '
|
|
127
|
+
'(is `node` on PATH and ~/.holon/runtime warm?)'));
|
|
128
|
+
await bridge.connect(
|
|
129
|
+
cloud: 'https://offline.invalid', // ignored: the offline runner serves everything locally
|
|
130
|
+
workspace: workspace,
|
|
131
|
+
clientId: clientId,
|
|
132
|
+
allowUnborn: true,
|
|
133
|
+
);
|
|
134
|
+
final harness =
|
|
135
|
+
NomosTestHarness._(bridge, workspace, transport, workdir, onDiagnostic);
|
|
136
|
+
if (autoTearDown && registerTearDown != null) {
|
|
137
|
+
registerTearDown(harness.dispose);
|
|
138
|
+
}
|
|
139
|
+
return harness;
|
|
140
|
+
} catch (_) {
|
|
141
|
+
// Setup failed — tear down deterministically so nothing is left running or on disk.
|
|
142
|
+
if (transport != null) {
|
|
143
|
+
try {
|
|
144
|
+
await transport.dispose();
|
|
145
|
+
} catch (_) {}
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await workdir.delete(recursive: true);
|
|
149
|
+
} catch (_) {}
|
|
150
|
+
rethrow;
|
|
101
151
|
}
|
|
102
152
|
}
|
|
103
153
|
|
|
104
|
-
///
|
|
105
|
-
///
|
|
106
|
-
|
|
107
|
-
String clientId = 'test-runner',
|
|
108
|
-
String? authToken,
|
|
109
|
-
String? authorSecret,
|
|
110
|
-
bool allowUnborn = false,
|
|
111
|
-
}) async {
|
|
112
|
-
if (_disposed) throw StateError('harness is disposed');
|
|
113
|
-
final transport = await NodeRunnerTransport.start(
|
|
114
|
-
entry: _nodeEntry,
|
|
115
|
-
resolveFrom: _resolveFrom,
|
|
116
|
-
onDiagnostic: _onDiagnostic,
|
|
117
|
-
);
|
|
118
|
-
_transports.add(transport);
|
|
119
|
-
final bridge = NomosBridge(transport);
|
|
120
|
-
await bridge.ready.timeout(const Duration(seconds: 30));
|
|
121
|
-
await bridge.connect(
|
|
122
|
-
cloud: cloud,
|
|
123
|
-
workspace: workspace,
|
|
124
|
-
clientId: clientId,
|
|
125
|
-
authToken: authToken ?? _authToken,
|
|
126
|
-
authorSecret: authorSecret,
|
|
127
|
-
allowUnborn: allowUnborn,
|
|
128
|
-
);
|
|
129
|
-
return bridge;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// Tear down every runner this harness spawned. The throwaway workspace's ledger stays on the cloud
|
|
133
|
-
/// (there is no open destroy lane — reclaim is law-gated); its unique name keeps it inert.
|
|
154
|
+
/// Deterministic, idempotent, total-ordered teardown: stop the runner (SIGTERM → 5s deadline → SIGKILL,
|
|
155
|
+
/// via [NodeRunnerTransport.dispose]), then delete the temp dir. Safe to call twice; safe to call after a
|
|
156
|
+
/// failed setup. Nothing left running, nothing left on disk.
|
|
134
157
|
Future<void> dispose() async {
|
|
135
158
|
if (_disposed) return;
|
|
136
159
|
_disposed = true;
|
|
137
|
-
|
|
138
|
-
await
|
|
160
|
+
try {
|
|
161
|
+
await _transport.dispose();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
_diag('[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
|
|
139
164
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Future<void> _create() async {
|
|
146
|
-
final r = await _post('/v2/workspaces/$workspace');
|
|
147
|
-
if (r.$1 >= 300 || !(r.$2['ok'] == true)) {
|
|
148
|
-
throw NomosTestHarnessException(
|
|
149
|
-
'workspace birth for $workspace refused (${r.$1}): ${_brief(r.$3)} — '
|
|
150
|
-
'a birth needs a principal (npx githolon login --agent, or pass principal:/authToken:)');
|
|
165
|
+
try {
|
|
166
|
+
if (await _workdir.exists()) await _workdir.delete(recursive: true);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
_diag('[nomos-test-harness] temp-dir cleanup error: $e');
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (_principal != null) req.headers.set('x-nomos-principal', _principal);
|
|
161
|
-
if (contentType != null) {
|
|
162
|
-
req.headers.contentType = ContentType.parse('$contentType; charset=utf-8');
|
|
163
|
-
}
|
|
164
|
-
if (body != null) req.add(utf8.encode(body));
|
|
165
|
-
final res = await req.close();
|
|
166
|
-
final text = await res.transform(utf8.decoder).join();
|
|
167
|
-
Map<String, Object?> decoded = const {};
|
|
168
|
-
try {
|
|
169
|
-
final d = jsonDecode(text);
|
|
170
|
-
if (d is Map) decoded = d.cast<String, Object?>();
|
|
171
|
-
} catch (_) {}
|
|
172
|
-
return (res.statusCode, decoded, text);
|
|
173
|
-
} finally {
|
|
174
|
-
client.close();
|
|
172
|
+
void _diag(String line) {
|
|
173
|
+
final f = _onDiagnostic;
|
|
174
|
+
if (f != null) {
|
|
175
|
+
f(line);
|
|
176
|
+
} else {
|
|
177
|
+
stderr.writeln(line);
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
// ── offline install (a short-lived node child; produces the restore snapshot) ──
|
|
182
|
+
|
|
183
|
+
static Future<void> _runInstall({
|
|
184
|
+
required String node,
|
|
185
|
+
required String helper,
|
|
186
|
+
required List<String> deployJsonPaths,
|
|
187
|
+
required String workspace,
|
|
188
|
+
required String snapshot,
|
|
189
|
+
String? runtimeDir,
|
|
190
|
+
void Function(String line)? onDiagnostic,
|
|
191
|
+
}) async {
|
|
192
|
+
if (!File(helper).existsSync()) {
|
|
193
|
+
throw NomosTestHarnessException(
|
|
194
|
+
'offline-install helper not found at $helper — pass installHelper: explicitly '
|
|
195
|
+
'(it ships beside node-entry.mjs)');
|
|
196
|
+
}
|
|
197
|
+
final r = await Process.run(node, [
|
|
198
|
+
helper,
|
|
199
|
+
'--deploy', deployJsonPaths.join(','),
|
|
200
|
+
'--workspace', workspace,
|
|
201
|
+
'--out', snapshot,
|
|
202
|
+
if (runtimeDir != null) ...['--runtime', runtimeDir],
|
|
203
|
+
]);
|
|
204
|
+
final err = (r.stderr as String).trim();
|
|
205
|
+
if (err.isNotEmpty) {
|
|
206
|
+
for (final line in err.split('\n')) {
|
|
207
|
+
(onDiagnostic ?? stderr.writeln)('[offline-install] $line');
|
|
208
|
+
}
|
|
186
209
|
}
|
|
187
|
-
|
|
188
|
-
p ??= t != null ? _jwtSub(t) : null;
|
|
189
|
-
if (p == null && t == null) {
|
|
210
|
+
if (r.exitCode != 0) {
|
|
190
211
|
throw NomosTestHarnessException(
|
|
191
|
-
'
|
|
192
|
-
'(the harness reads ~/.holon/credentials.json) or pass principal:/authToken:');
|
|
212
|
+
'offline law install failed (exit ${r.exitCode}): ${err.isEmpty ? "(no stderr)" : err}');
|
|
193
213
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
// The helper prints one JSON line on success; verify it before spawning the runner.
|
|
215
|
+
final out = (r.stdout as String).trim();
|
|
216
|
+
final last = out.isEmpty ? '' : out.split('\n').last;
|
|
217
|
+
Map<String, Object?> decoded = const {};
|
|
198
218
|
try {
|
|
199
|
-
final
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
final session = (d['session'] as Map?)?.cast<String, Object?>();
|
|
206
|
-
return (
|
|
207
|
-
principal: session?['uid']?.toString() ?? d['principal']?.toString(),
|
|
208
|
-
authToken: session?['accessToken']?.toString(),
|
|
209
|
-
);
|
|
210
|
-
} catch (_) {
|
|
211
|
-
return (principal: null, authToken: null);
|
|
219
|
+
final d = jsonDecode(last);
|
|
220
|
+
if (d is Map) decoded = d.cast<String, Object?>();
|
|
221
|
+
} catch (_) {}
|
|
222
|
+
if (decoded['ok'] != true || !File(snapshot).existsSync()) {
|
|
223
|
+
throw NomosTestHarnessException(
|
|
224
|
+
'offline law install produced no snapshot (stdout: ${out.isEmpty ? "(empty)" : out})');
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
static String
|
|
216
|
-
final
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
static Future<String> _resolveEntry(String node, String from) async {
|
|
229
|
+
final r = await Process.run(
|
|
230
|
+
node,
|
|
231
|
+
['-p', "require.resolve('@githolon/client/runner/node-entry.mjs')"],
|
|
232
|
+
workingDirectory: from,
|
|
233
|
+
);
|
|
234
|
+
final out = (r.stdout as String).trim();
|
|
235
|
+
if (r.exitCode != 0 || out.isEmpty) {
|
|
236
|
+
throw NomosTestHarnessException(
|
|
237
|
+
'cannot resolve @githolon/client/runner/node-entry.mjs from $from — '
|
|
238
|
+
'is @githolon/client installed (npm install)? Pass nodeEntry: to override. stderr: ${r.stderr}');
|
|
225
239
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
static String _stripSlash(String s) =>
|
|
229
|
-
s.endsWith('/') ? s.substring(0, s.length - 1) : s;
|
|
230
|
-
|
|
231
|
-
static String _hex(int n) {
|
|
232
|
-
final rng = Random.secure();
|
|
233
|
-
return List.generate(n, (_) => rng.nextInt(16).toRadixString(16)).join();
|
|
240
|
+
return out;
|
|
234
241
|
}
|
|
235
242
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// NOMOS — TYPED ERROR MINTING (§7 structured runtime error DX). `nomosErrorFromInfo` routes a structured
|
|
2
|
+
// `NomosErrorInfo` to the MOST SPECIFIC typed class by stable code prefix, so an app's `errorBuilder` branches
|
|
3
|
+
// on `is` and reads structured fields (not scraped prose). Every typed error still extends `NomosBridgeError`.
|
|
4
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
5
|
+
import 'package:test/test.dart';
|
|
6
|
+
|
|
7
|
+
NomosErrorInfo info(String code, {Map<String, Object?> attrs = const {}, String? workspace, String? operation}) =>
|
|
8
|
+
NomosErrorInfo(code: code, message: 'refused', workspace: workspace, operation: operation, attributes: attrs);
|
|
9
|
+
|
|
10
|
+
void main() {
|
|
11
|
+
test('law.* mints LawMismatchError with both sides + compat verdict + the exact message', () {
|
|
12
|
+
final e = nomosErrorFromInfo(info(
|
|
13
|
+
'nomos.law.mismatch',
|
|
14
|
+
workspace: 'ws-1',
|
|
15
|
+
operation: 'signGuestbook',
|
|
16
|
+
attrs: {
|
|
17
|
+
'nomos.law.generatedFor': 'aaaa',
|
|
18
|
+
'nomos.law.workspaceRuns': 'bbbb',
|
|
19
|
+
'nomos.law.structurallyCompatible': false,
|
|
20
|
+
'nomos.law.incompatibleMember': 'mood',
|
|
21
|
+
},
|
|
22
|
+
));
|
|
23
|
+
expect(e, isA<NomosBridgeError>()); // still catchable generically
|
|
24
|
+
expect(e, isA<LawMismatchError>());
|
|
25
|
+
final m = e as LawMismatchError;
|
|
26
|
+
expect(m.code, 'nomos.law.mismatch');
|
|
27
|
+
expect(m.workspace, 'ws-1');
|
|
28
|
+
expect(m.generatedFor, 'aaaa');
|
|
29
|
+
expect(m.workspaceRuns, 'bbbb');
|
|
30
|
+
expect(m.operation, 'signGuestbook');
|
|
31
|
+
expect(m.structurallyCompatible, isFalse);
|
|
32
|
+
expect(m.incompatibleMember, 'mood');
|
|
33
|
+
expect(m.recommendedAction, NomosRecommendedAction.recompileClient);
|
|
34
|
+
expect(
|
|
35
|
+
m.mismatchMessage,
|
|
36
|
+
'client generated for law aaaa, workspace runs law bbbb; structurally incompatible on mood — '
|
|
37
|
+
'recompile the client (githolon compile / npx nomos-compile) against the deployed law',
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('law.* compatible verdict says "proceeding"', () {
|
|
42
|
+
final m = nomosErrorFromInfo(info('nomos.law.drift', attrs: {
|
|
43
|
+
'nomos.law.generatedFor': 'aaaa',
|
|
44
|
+
'nomos.law.workspaceRuns': 'bbbb',
|
|
45
|
+
'nomos.law.structurallyCompatible': true,
|
|
46
|
+
})) as LawMismatchError;
|
|
47
|
+
expect(m.mismatchMessage, startsWith('client generated for law aaaa, workspace runs law bbbb; structurally compatible — proceeding —'));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('auth.* mints AuthError with reauth as the remedy', () {
|
|
51
|
+
final e = nomosErrorFromInfo(info('nomos.auth.issuer_mismatch', workspace: 'ws-1', attrs: {'nomos.auth.issuer': 'https://evil'}));
|
|
52
|
+
expect(e, isA<AuthError>());
|
|
53
|
+
final a = e as AuthError;
|
|
54
|
+
expect(a.code, 'nomos.auth.issuer_mismatch');
|
|
55
|
+
expect(a.issuer, 'https://evil');
|
|
56
|
+
expect(a.recommendedAction, NomosRecommendedAction.reauth);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('load.* mints LoadError carrying the artifact + source URL', () {
|
|
60
|
+
final e = nomosErrorFromInfo(info('nomos.load.wasm_fetch_failed', attrs: {
|
|
61
|
+
'nomos.load.artifact': 'holon.wasm',
|
|
62
|
+
'nomos.load.sourceUrl': 'https://nomos/runtime/holon.wasm',
|
|
63
|
+
}));
|
|
64
|
+
expect(e, isA<LoadError>());
|
|
65
|
+
final l = e as LoadError;
|
|
66
|
+
expect(l.artifact, 'holon.wasm');
|
|
67
|
+
expect(l.sourceUrl, 'https://nomos/runtime/holon.wasm');
|
|
68
|
+
expect(l.recommendedAction, NomosRecommendedAction.retry);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('custody.* still mints CustodyError (unchanged), unknown codes stay generic', () {
|
|
72
|
+
expect(nomosErrorFromInfo(info('nomos.custody.missing_object')), isA<CustodyError>());
|
|
73
|
+
final g = nomosErrorFromInfo(info('nomos.bridge.error'));
|
|
74
|
+
expect(g, isA<NomosBridgeError>());
|
|
75
|
+
expect(g, isNot(isA<LawMismatchError>()));
|
|
76
|
+
});
|
|
77
|
+
}
|