@feasibleone/blong-gogo 1.15.0 → 1.16.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/CHANGELOG.md +8 -0
- package/package.json +7 -3
- package/src/ConfigRuntime.test.ts +363 -0
- package/src/ConfigRuntime.ts +384 -0
- package/src/Gateway.ts +6 -2
- package/src/Registry.ts +42 -7
- package/src/Remote.ts +1 -1
- package/src/RpcClient.ts +7 -1
- package/src/RpcServer.ts +16 -7
- package/src/Watch.ts +76 -4
- package/src/adapter/server/http.ts +21 -0
- package/src/adapter/server/knex.ts +18 -0
- package/src/adapter.ts +2 -1
- package/src/chain.ts +5 -5
- package/src/checkpoint.ts +32 -0
- package/src/codec/adapter/jsonrpc/receive.ts +6 -1
- package/src/codec/test/test/testLoginTokenCreate.ts +13 -14
- package/src/jose.ts +47 -5
- package/src/layerProxy.ts +277 -26
- package/src/lib.test.ts +102 -0
- package/src/lib.ts +36 -0
- package/src/load.ts +56 -21
- package/src/mle.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.16.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.15.0...blong-gogo-v1.16.0) (2026-04-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* implement ConfigRuntime for hot configuration reload ([#115](https://github.com/feasibleone/blong/issues/115)) ([61bab7c](https://github.com/feasibleone/blong/commit/61bab7ce8587bf83d72fe80138aaef68943f21c4))
|
|
9
|
+
* unified test and handlers ([#117](https://github.com/feasibleone/blong/issues/117)) ([bf0ed96](https://github.com/feasibleone/blong/commit/bf0ed96c5df3d949fa225dd8a30fc25698a7855a))
|
|
10
|
+
|
|
3
11
|
## [1.15.0](https://github.com/feasibleone/blong/compare/blong-gogo-v1.14.3...blong-gogo-v1.15.0) (2026-03-29)
|
|
4
12
|
|
|
5
13
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feasibleone/blong-gogo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"url": "git+https://github.com/feasibleone/blong.git"
|
|
6
6
|
},
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/load.ts"
|
|
9
|
+
".": "./src/load.ts",
|
|
10
|
+
"./ConfigRuntime.js": "./src/ConfigRuntime.ts"
|
|
10
11
|
},
|
|
11
12
|
"bin": {
|
|
12
13
|
"blong": "./bin/blong.ts",
|
|
@@ -69,11 +70,14 @@
|
|
|
69
70
|
"@rushstack/heft": "^1.2.6",
|
|
70
71
|
"@rushstack/heft-lint-plugin": "^1.2.6",
|
|
71
72
|
"@rushstack/heft-typescript-plugin": "^1.3.1",
|
|
73
|
+
"@types/node": "^24",
|
|
72
74
|
"eslint": "~9.39.2",
|
|
75
|
+
"tap": "^21.6.2",
|
|
73
76
|
"typescript": "^5.9.3"
|
|
74
77
|
},
|
|
75
78
|
"scripts": {
|
|
76
79
|
"build": "true",
|
|
77
|
-
"ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance"
|
|
80
|
+
"ci-publish": "node ../../common/scripts/install-run-rush-pnpm.js publish --access public --provenance",
|
|
81
|
+
"ci-unit": "tap src/ConfigRuntime.test.ts src/lib.test.ts --allow-incomplete-coverage"
|
|
78
82
|
}
|
|
79
83
|
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for ConfigRuntime — the authoritative config lifecycle owner.
|
|
3
|
+
*
|
|
4
|
+
* Tests validate:
|
|
5
|
+
* 1. deepDiff — flat diff of two plain objects (added, removed, modified)
|
|
6
|
+
* 2. createConfigProxy — stable proxy with live-updating backing store
|
|
7
|
+
* 3. affectedNamespaces — maps diff keys to port namespaces
|
|
8
|
+
* 4. Destructuring safety checks — verifies that:
|
|
9
|
+
* ✅ partial destructuring (sub-object level) uses path-based proxies that
|
|
10
|
+
* stay live across updates — reads reflect the current value at call time
|
|
11
|
+
* ❌ full destructuring (scalar level) captures a stale value at load time
|
|
12
|
+
* 5. Factory phase guard — enterConfigFactoryPhase / exitConfigFactoryPhase:
|
|
13
|
+
* throws on primitive reads in 'throw' mode (default)
|
|
14
|
+
* collects errors in 'collect' mode (for test verification)
|
|
15
|
+
* silent for undefined / sub-object reads regardless of mode
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {test} from 'tap';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
affectedNamespaces,
|
|
22
|
+
createConfigProxy,
|
|
23
|
+
deepDiff,
|
|
24
|
+
enterConfigFactoryPhase,
|
|
25
|
+
exitConfigFactoryPhase,
|
|
26
|
+
} from './ConfigRuntime.ts';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// deepDiff
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
test('deepDiff — identical objects produce empty diff', async t => {
|
|
33
|
+
const prev = {a: 1, b: {c: 2}};
|
|
34
|
+
const next = {a: 1, b: {c: 2}};
|
|
35
|
+
const diff = deepDiff(prev, next);
|
|
36
|
+
t.equal(diff.size, 0, 'no changes expected');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('deepDiff — modified leaf value is detected', async t => {
|
|
40
|
+
const prev = {db: {host: 'localhost', port: 5432}};
|
|
41
|
+
const next = {db: {host: '10.0.0.1', port: 5432}};
|
|
42
|
+
const diff = deepDiff(prev, next);
|
|
43
|
+
t.equal(diff.size, 1, 'one change expected');
|
|
44
|
+
t.ok(diff.has('db.host'), 'db.host change detected');
|
|
45
|
+
t.equal(diff.get('db.host')!.prev, 'localhost');
|
|
46
|
+
t.equal(diff.get('db.host')!.next, '10.0.0.1');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('deepDiff — added key is detected', async t => {
|
|
50
|
+
const prev = {a: 1};
|
|
51
|
+
const next = {a: 1, b: 2};
|
|
52
|
+
const diff = deepDiff(prev, next);
|
|
53
|
+
t.equal(diff.size, 1, 'one addition expected');
|
|
54
|
+
t.ok(diff.has('b'));
|
|
55
|
+
t.equal(diff.get('b')!.prev, undefined);
|
|
56
|
+
t.equal(diff.get('b')!.next, 2);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('deepDiff — removed key is detected', async t => {
|
|
60
|
+
const prev = {a: 1, b: 2};
|
|
61
|
+
const next = {a: 1};
|
|
62
|
+
const diff = deepDiff(prev, next);
|
|
63
|
+
t.equal(diff.size, 1, 'one removal expected');
|
|
64
|
+
t.ok(diff.has('b'));
|
|
65
|
+
t.equal(diff.get('b')!.prev, 2);
|
|
66
|
+
t.equal(diff.get('b')!.next, undefined);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('deepDiff — nested changes produce dotted paths', async t => {
|
|
70
|
+
const prev = {tls: {cert: 'old-cert', key: 'old-key'}};
|
|
71
|
+
const next = {tls: {cert: 'new-cert', key: 'old-key'}};
|
|
72
|
+
const diff = deepDiff(prev, next);
|
|
73
|
+
t.equal(diff.size, 1);
|
|
74
|
+
t.ok(diff.has('tls.cert'));
|
|
75
|
+
t.equal(diff.get('tls.cert')!.next, 'new-cert');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('deepDiff — array changes are detected as leaf diffs', async t => {
|
|
79
|
+
const prev = {allowed: ['a', 'b']};
|
|
80
|
+
const next = {allowed: ['a', 'b', 'c']};
|
|
81
|
+
const diff = deepDiff(prev, next);
|
|
82
|
+
t.equal(diff.size, 1, 'array change detected as single leaf');
|
|
83
|
+
t.ok(diff.has('allowed'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('deepDiff — deeply nested multi-change', async t => {
|
|
87
|
+
const prev = {db: {host: 'h1', port: 3306, ssl: {ca: 'old-ca'}}};
|
|
88
|
+
const next = {db: {host: 'h2', port: 3306, ssl: {ca: 'new-ca'}}};
|
|
89
|
+
const diff = deepDiff(prev, next);
|
|
90
|
+
t.equal(diff.size, 2, 'two changes: host and ssl.ca');
|
|
91
|
+
t.ok(diff.has('db.host'));
|
|
92
|
+
t.ok(diff.has('db.ssl.ca'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// createConfigProxy
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
test('createConfigProxy — reads current values', async t => {
|
|
100
|
+
const store = {db: {host: 'localhost', port: 5432}};
|
|
101
|
+
const {proxy} = createConfigProxy(store);
|
|
102
|
+
t.equal((proxy as any).db.host, 'localhost');
|
|
103
|
+
t.equal((proxy as any).db.port, 5432);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('createConfigProxy — reflects updated values after update()', async t => {
|
|
107
|
+
const store = {db: {host: 'localhost'}};
|
|
108
|
+
const {proxy, update} = createConfigProxy(store);
|
|
109
|
+
|
|
110
|
+
t.equal((proxy as any).db.host, 'localhost');
|
|
111
|
+
|
|
112
|
+
update({db: {host: '10.0.0.1'}} as any);
|
|
113
|
+
|
|
114
|
+
t.equal((proxy as any).db.host, '10.0.0.1', 'proxy reflects updated value');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('createConfigProxy — proxy reference remains stable across updates', async t => {
|
|
118
|
+
const store = {a: 1};
|
|
119
|
+
const {proxy, update} = createConfigProxy(store);
|
|
120
|
+
|
|
121
|
+
const ref1 = proxy;
|
|
122
|
+
update({a: 2} as any);
|
|
123
|
+
const ref2 = proxy;
|
|
124
|
+
|
|
125
|
+
t.equal(ref1, ref2, 'same proxy reference after update');
|
|
126
|
+
t.equal((ref1 as any).a, 2, 'old reference reflects new value');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('createConfigProxy — has/ownKeys/enumeration work correctly', async t => {
|
|
130
|
+
const store = {x: 1, y: 2};
|
|
131
|
+
const {proxy} = createConfigProxy(store);
|
|
132
|
+
|
|
133
|
+
t.ok('x' in proxy, 'has "x"');
|
|
134
|
+
t.ok('y' in proxy, 'has "y"');
|
|
135
|
+
t.notOk('z' in proxy, 'does not have "z"');
|
|
136
|
+
|
|
137
|
+
const keys = Object.keys(proxy);
|
|
138
|
+
t.ok(keys.includes('x'), 'ownKeys includes "x"');
|
|
139
|
+
t.ok(keys.includes('y'), 'ownKeys includes "y"');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('createConfigProxy — undefined properties return undefined', async t => {
|
|
143
|
+
const store = {a: 1};
|
|
144
|
+
const {proxy} = createConfigProxy(store);
|
|
145
|
+
t.equal((proxy as any).nonExistent, undefined);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// affectedNamespaces
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
test('affectedNamespaces — exact key match', async t => {
|
|
153
|
+
const diff = new Map([['payment.adapter.db', {prev: 1, next: 2}]]);
|
|
154
|
+
const ports = ['payment.adapter.db', 'user.adapter.db'];
|
|
155
|
+
const affected = affectedNamespaces(diff, ports);
|
|
156
|
+
t.ok(affected.has('payment.adapter.db'), 'exact match detected');
|
|
157
|
+
t.notOk(affected.has('user.adapter.db'), 'unrelated port excluded');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('affectedNamespaces — prefix match', async t => {
|
|
161
|
+
const diff = new Map([['payment.adapter.db.host', {prev: 'a', next: 'b'}]]);
|
|
162
|
+
const ports = ['payment.adapter.db', 'user.adapter.db'];
|
|
163
|
+
const affected = affectedNamespaces(diff, ports);
|
|
164
|
+
t.ok(affected.has('payment.adapter.db'), 'prefix match detected');
|
|
165
|
+
t.notOk(affected.has('user.adapter.db'), 'unrelated port excluded');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('affectedNamespaces — no match returns empty set', async t => {
|
|
169
|
+
const diff = new Map([['unrelated.key', {prev: 1, next: 2}]]);
|
|
170
|
+
const ports = ['payment.adapter.db'];
|
|
171
|
+
const affected = affectedNamespaces(diff, ports);
|
|
172
|
+
t.equal(affected.size, 0, 'no affected ports');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('affectedNamespaces — multiple ports can be affected', async t => {
|
|
176
|
+
const diff = new Map([
|
|
177
|
+
['payment.adapter.db.host', {prev: 'old', next: 'new'}],
|
|
178
|
+
['user.adapter.db.port', {prev: 3306, next: 5432}],
|
|
179
|
+
]);
|
|
180
|
+
const ports = ['payment.adapter.db', 'user.adapter.db', 'audit.adapter.kafka'];
|
|
181
|
+
const affected = affectedNamespaces(diff, ports);
|
|
182
|
+
t.equal(affected.size, 2, 'two ports affected');
|
|
183
|
+
t.ok(affected.has('payment.adapter.db'));
|
|
184
|
+
t.ok(affected.has('user.adapter.db'));
|
|
185
|
+
t.notOk(affected.has('audit.adapter.kafka'));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('affectedNamespaces — partial name prefix does not match', async t => {
|
|
189
|
+
// 'payment.adapter.db2' should NOT match port 'payment.adapter.db'
|
|
190
|
+
const diff = new Map([['payment.adapter.db2.host', {prev: 'a', next: 'b'}]]);
|
|
191
|
+
const ports = ['payment.adapter.db'];
|
|
192
|
+
const affected = affectedNamespaces(diff, ports);
|
|
193
|
+
t.equal(affected.size, 0, 'partial-name prefix should not match');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Destructuring safety checks
|
|
198
|
+
//
|
|
199
|
+
// These tests validate the proxy-access rule for handler config arguments:
|
|
200
|
+
//
|
|
201
|
+
// ✅ Safe — partial destructuring to an intermediate object:
|
|
202
|
+
// handler(({ config: { theme } }) => ({ myFn: () => theme.name }))
|
|
203
|
+
// `theme` is a path-based proxy; every property read on it re-traverses
|
|
204
|
+
// the root `current`, so `theme.name` always returns the current value
|
|
205
|
+
// even after a config reload.
|
|
206
|
+
//
|
|
207
|
+
// ❌ Unsafe — full destructuring to a scalar at factory time:
|
|
208
|
+
// handler(({ config: { theme: { name } } }) => ({ myFn: () => name }))
|
|
209
|
+
// `name` is a primitive captured at module-load time; it will NOT reflect
|
|
210
|
+
// later config changes.
|
|
211
|
+
//
|
|
212
|
+
// To make this testable without the full framework, the tests simulate the
|
|
213
|
+
// handler factory pattern using raw createConfigProxy calls and a mock factory
|
|
214
|
+
// function that mirrors the two patterns.
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
test('partial destructuring — theme sub-object is a live proxy node that reflects current values after update', async t => {
|
|
218
|
+
const initialStore = {theme: {name: 'light', mode: 'day'}};
|
|
219
|
+
const {proxy, update} = createConfigProxy(initialStore);
|
|
220
|
+
|
|
221
|
+
// Simulate factory-time partial destructuring: extract the `theme` sub-object
|
|
222
|
+
// (as would happen in `handler(({ config: { theme } }) => ...)`)
|
|
223
|
+
const {theme} = proxy as {theme: {name: string; mode: string}};
|
|
224
|
+
|
|
225
|
+
// --- Verify initial read through the partially-destructured sub-object ---
|
|
226
|
+
t.equal(theme.name, 'light', 'initial theme.name read through sub-object');
|
|
227
|
+
t.equal(theme.mode, 'day', 'initial theme.mode read through sub-object');
|
|
228
|
+
|
|
229
|
+
// --- Simulate a config reload by updating the root proxy backing store ---
|
|
230
|
+
update({theme: {name: 'dark', mode: 'night'}} as typeof initialStore);
|
|
231
|
+
|
|
232
|
+
// --- Path-based proxies: the captured sub-proxy is a live view over the
|
|
233
|
+
// root `current` cell, so it reflects the new values after update() ---
|
|
234
|
+
t.equal(theme.name, 'dark', 'sub-proxy theme.name reflects update via path traversal');
|
|
235
|
+
t.equal(theme.mode, 'night', 'sub-proxy theme.mode reflects update via path traversal');
|
|
236
|
+
t.equal((proxy as any).theme.name, 'dark', 'root proxy.theme.name also reflects update');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('full destructuring — scalar captured at factory time does NOT reflect later updates', async t => {
|
|
240
|
+
const initialStore = {theme: {name: 'light'}};
|
|
241
|
+
const {proxy, update} = createConfigProxy(initialStore);
|
|
242
|
+
|
|
243
|
+
// Simulate the UNSAFE pattern: extract a leaf primitive at factory time
|
|
244
|
+
// (as would happen in `handler(({ config: { theme: { name } } }) => ...)`)
|
|
245
|
+
const {
|
|
246
|
+
theme: {name: capturedName},
|
|
247
|
+
} = proxy as {theme: {name: string}};
|
|
248
|
+
|
|
249
|
+
t.equal(capturedName, 'light', 'initial captured value is "light"');
|
|
250
|
+
|
|
251
|
+
// Simulate a config reload
|
|
252
|
+
update({theme: {name: 'dark'}} as typeof initialStore);
|
|
253
|
+
|
|
254
|
+
// The captured scalar is stale — it still reads 'light'
|
|
255
|
+
t.equal(capturedName, 'light', 'captured scalar is stale after config update');
|
|
256
|
+
|
|
257
|
+
// Root proxy reflects the new value
|
|
258
|
+
t.equal((proxy as any).theme.name, 'dark', 'root proxy reflects updated value');
|
|
259
|
+
|
|
260
|
+
// This proves: NEVER destructure leaf primitives at handler factory time.
|
|
261
|
+
t.not(
|
|
262
|
+
capturedName,
|
|
263
|
+
'dark',
|
|
264
|
+
'primitive captured at load time does not update — confirmed anti-pattern',
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('root proxy access — always reflects current values regardless of nesting', async t => {
|
|
269
|
+
const initialStore = {theme: {name: 'light'}, greeting: 'hello'};
|
|
270
|
+
const {proxy, update} = createConfigProxy(initialStore);
|
|
271
|
+
|
|
272
|
+
// Simulate the SAFE pattern: hold a reference to the root proxy and access
|
|
273
|
+
// through it at call time (as in `handler(({ config }) => ({ fn: () => config.theme.name }))`)
|
|
274
|
+
const config = proxy as {theme: {name: string}; greeting: string};
|
|
275
|
+
|
|
276
|
+
t.equal(config.theme.name, 'light', 'initial root access');
|
|
277
|
+
t.equal(config.greeting, 'hello', 'initial greeting via root');
|
|
278
|
+
|
|
279
|
+
update({theme: {name: 'dark'}, greeting: 'hi'} as typeof initialStore);
|
|
280
|
+
|
|
281
|
+
t.equal(config.theme.name, 'dark', 'root proxy.theme.name reflects update');
|
|
282
|
+
t.equal(config.greeting, 'hi', 'root proxy.greeting reflects update');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Factory phase guard — enterConfigFactoryPhase / exitConfigFactoryPhase
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
test('factory phase guard — throws on primitive read in default throw mode', async t => {
|
|
290
|
+
const {proxy} = createConfigProxy({host: 'localhost', port: 5432});
|
|
291
|
+
const p = proxy as any;
|
|
292
|
+
|
|
293
|
+
enterConfigFactoryPhase(); // default mode = 'throw'
|
|
294
|
+
t.throws(
|
|
295
|
+
() => p.host,
|
|
296
|
+
/anti-pattern/,
|
|
297
|
+
'reading a primitive during factory phase throws by default',
|
|
298
|
+
);
|
|
299
|
+
exitConfigFactoryPhase(); // always clean up
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('factory phase guard — collects errors in collect mode without throwing', async t => {
|
|
303
|
+
const {proxy} = createConfigProxy({host: 'localhost', port: 5432});
|
|
304
|
+
const p = proxy as any;
|
|
305
|
+
|
|
306
|
+
enterConfigFactoryPhase('collect');
|
|
307
|
+
t.doesNotThrow(() => p.host, 'no throw in collect mode');
|
|
308
|
+
t.doesNotThrow(() => p.port, 'no throw for second read in collect mode');
|
|
309
|
+
const errors = exitConfigFactoryPhase();
|
|
310
|
+
|
|
311
|
+
t.equal(errors.length, 2, 'two primitive reads were recorded');
|
|
312
|
+
t.match(errors[0].message, /anti-pattern/, 'first error mentions anti-pattern');
|
|
313
|
+
t.match(errors[0].message, /host/, 'first error names the offending key');
|
|
314
|
+
t.match(errors[1].message, /port/, 'second error names the offending key');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('factory phase guard — sub-object read is NOT flagged (safe partial destructuring)', async t => {
|
|
318
|
+
const {proxy} = createConfigProxy({theme: {name: 'light'}});
|
|
319
|
+
const p = proxy as any;
|
|
320
|
+
|
|
321
|
+
enterConfigFactoryPhase(); // throw mode
|
|
322
|
+
t.doesNotThrow(
|
|
323
|
+
() => p.theme, // returns a sub-proxy (object), not a primitive
|
|
324
|
+
'accessing a nested object during factory phase is safe',
|
|
325
|
+
);
|
|
326
|
+
exitConfigFactoryPhase();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('factory phase guard — undefined read is NOT flagged', async t => {
|
|
330
|
+
const {proxy} = createConfigProxy({a: 1});
|
|
331
|
+
const p = proxy as any;
|
|
332
|
+
|
|
333
|
+
enterConfigFactoryPhase();
|
|
334
|
+
t.doesNotThrow(
|
|
335
|
+
() => p.nonExistent, // undefined is safe to read (nothing to capture)
|
|
336
|
+
'undefined key access during factory phase does not throw',
|
|
337
|
+
);
|
|
338
|
+
exitConfigFactoryPhase();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test('factory phase guard — guard is inactive after exitConfigFactoryPhase', async t => {
|
|
342
|
+
const {proxy} = createConfigProxy({host: 'localhost'});
|
|
343
|
+
const p = proxy as any;
|
|
344
|
+
|
|
345
|
+
enterConfigFactoryPhase();
|
|
346
|
+
exitConfigFactoryPhase(); // exit immediately
|
|
347
|
+
|
|
348
|
+
t.doesNotThrow(() => p.host, 'primitive read after exit is safe again');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test('factory phase guard — exitConfigFactoryPhase returns empty array in throw mode', async t => {
|
|
352
|
+
const {proxy} = createConfigProxy({host: 'localhost'});
|
|
353
|
+
const p = proxy as any;
|
|
354
|
+
|
|
355
|
+
enterConfigFactoryPhase(); // throw mode — errors are not collected
|
|
356
|
+
try {
|
|
357
|
+
p.host; // would throw
|
|
358
|
+
} catch (_) {
|
|
359
|
+
// expected
|
|
360
|
+
}
|
|
361
|
+
const errors = exitConfigFactoryPhase();
|
|
362
|
+
t.equal(errors.length, 0, 'no errors collected in throw mode (they were thrown)');
|
|
363
|
+
});
|