@bespokeagentics/microdots-host 0.1.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/LICENSE +202 -0
- package/package.json +25 -0
- package/src/index.ts +144 -0
- package/src/loader.ts +119 -0
- package/src/mounting.test.ts +214 -0
- package/src/mounting.ts +90 -0
- package/src/placementChecks.test.ts +593 -0
- package/src/placementChecks.ts +428 -0
- package/src/registry.ts +63 -0
- package/src/routes.ts +156 -0
- package/src/rules.test.ts +598 -0
- package/src/rules.ts +448 -0
- package/src/slots.test.ts +46 -0
- package/src/slots.ts +58 -0
- package/src/wire.test.ts +631 -0
- package/src/wire.ts +560 -0
- package/src/wireEngine.test.ts +810 -0
- package/src/wireEngine.ts +468 -0
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import { Option } from 'effect'
|
|
2
|
+
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { Wire } from './wire.ts'
|
|
5
|
+
import {
|
|
6
|
+
type WireEngine,
|
|
7
|
+
type WireEngineOptions,
|
|
8
|
+
type WireTrace,
|
|
9
|
+
applyTransform,
|
|
10
|
+
attachWireEngine,
|
|
11
|
+
planWires,
|
|
12
|
+
} from './wireEngine.ts'
|
|
13
|
+
|
|
14
|
+
/* ============================================================
|
|
15
|
+
The recorded demo-host wires — w1/w2/w3 exactly as
|
|
16
|
+
`apps/host/src/entry.ts` hand-wrote them before Phase 4.
|
|
17
|
+
============================================================ */
|
|
18
|
+
|
|
19
|
+
const w1: Wire = {
|
|
20
|
+
id: 'w1',
|
|
21
|
+
from: 'price-ticker',
|
|
22
|
+
event: 'quote-changed',
|
|
23
|
+
field: 'symbol',
|
|
24
|
+
fieldType: 'string',
|
|
25
|
+
to: 'fleet-health',
|
|
26
|
+
input: 'region',
|
|
27
|
+
inputType: 'string',
|
|
28
|
+
transform: {
|
|
29
|
+
_tag: 'lookup',
|
|
30
|
+
rows: { FOLD: 'us-east', EFCT: 'eu-west', ELMX: 'ap-south-1' },
|
|
31
|
+
fallback: 'us-east',
|
|
32
|
+
},
|
|
33
|
+
envs: ['dev', 'preview', 'prod'],
|
|
34
|
+
plain: 'When the quote changes, point the fleet at the symbol’s region.',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const w2: Wire = {
|
|
38
|
+
id: 'w2',
|
|
39
|
+
from: 'bespoke-contact-form',
|
|
40
|
+
event: 'submission-created',
|
|
41
|
+
field: 'id',
|
|
42
|
+
fieldType: 'string',
|
|
43
|
+
to: 'bespoke-contact-dashboard',
|
|
44
|
+
input: 'refresh-token',
|
|
45
|
+
inputType: 'string',
|
|
46
|
+
transform: { _tag: 'direct' },
|
|
47
|
+
envs: ['dev', 'preview', 'prod'],
|
|
48
|
+
plain: 'When a submission is stored, nudge the dashboard to reload.',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const w3: Wire = {
|
|
52
|
+
id: 'w3',
|
|
53
|
+
from: 'bespoke-contact-dashboard',
|
|
54
|
+
event: 'definition-changed',
|
|
55
|
+
field: 'version',
|
|
56
|
+
fieldType: 'number',
|
|
57
|
+
to: 'bespoke-contact-form',
|
|
58
|
+
input: 'definition-token',
|
|
59
|
+
inputType: 'number',
|
|
60
|
+
transform: { _tag: 'direct' },
|
|
61
|
+
envs: ['dev', 'preview', 'prod'],
|
|
62
|
+
plain: 'When the definition changes, nudge the form to refetch it.',
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEMO_WIRES: ReadonlyArray<Wire> = [w1, w2, w3]
|
|
66
|
+
|
|
67
|
+
/* ============================================================
|
|
68
|
+
The `host-changed` collision — pw12 and pg6/pg7, copied field-for-field
|
|
69
|
+
from `apps/platform/host-topology.json`. TWO dots emit `host-changed`
|
|
70
|
+
(`wiring-canvas` and `pages-rail`, `microdots/wiring/src/surface.ts:30` and
|
|
71
|
+
`microdots/pages/src/surface.ts:49`) with an identically shaped `{hostId}`
|
|
72
|
+
payload, and three panes across two screens expose a `host-id` attribute.
|
|
73
|
+
Both screens stay mounted — `activate` only toggles `section.hidden` — so
|
|
74
|
+
a name-only match crossed them.
|
|
75
|
+
============================================================ */
|
|
76
|
+
|
|
77
|
+
const pw12: Wire = {
|
|
78
|
+
id: 'pw12',
|
|
79
|
+
from: 'wiring-canvas',
|
|
80
|
+
event: 'host-changed',
|
|
81
|
+
field: 'hostId',
|
|
82
|
+
fieldType: 'string',
|
|
83
|
+
to: 'wiring-table',
|
|
84
|
+
input: 'host-id',
|
|
85
|
+
inputType: 'string',
|
|
86
|
+
transform: { _tag: 'direct' },
|
|
87
|
+
envs: ['dev', 'preview', 'prod'],
|
|
88
|
+
plain: 'When the canvas switches host, list that host’s wires.',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const pg6: Wire = {
|
|
92
|
+
id: 'pg6',
|
|
93
|
+
from: 'pages-rail',
|
|
94
|
+
event: 'host-changed',
|
|
95
|
+
field: 'hostId',
|
|
96
|
+
fieldType: 'string',
|
|
97
|
+
to: 'pages-canvas',
|
|
98
|
+
input: 'host-id',
|
|
99
|
+
inputType: 'string',
|
|
100
|
+
transform: { _tag: 'direct' },
|
|
101
|
+
envs: ['dev', 'preview', 'prod'],
|
|
102
|
+
plain: 'When the rail switches host, draw that host’s routes.',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pg7: Wire = { ...pg6, id: 'pg7', to: 'pages-inspector' }
|
|
106
|
+
|
|
107
|
+
/** A `json` wire — the one declared fieldType the demo records never use. */
|
|
108
|
+
const wJson: Wire = {
|
|
109
|
+
id: 'w-json',
|
|
110
|
+
from: 'bespoke-contact-dashboard',
|
|
111
|
+
event: 'snapshot-changed',
|
|
112
|
+
field: 'snapshot',
|
|
113
|
+
fieldType: 'json',
|
|
114
|
+
to: 'fleet-health',
|
|
115
|
+
input: 'snapshot',
|
|
116
|
+
inputType: 'json',
|
|
117
|
+
transform: { _tag: 'direct' },
|
|
118
|
+
envs: ['dev', 'preview', 'prod'],
|
|
119
|
+
plain: 'When the snapshot changes, hand the fleet the whole document.',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const enginesToDetach: Array<{ detach: () => void }> = []
|
|
123
|
+
|
|
124
|
+
/** Every DOM test runs the shells' environment, `dev`. */
|
|
125
|
+
const attach = (options: Omit<WireEngineOptions, 'env'>): WireEngine => {
|
|
126
|
+
const engine = attachWireEngine({ ...options, env: 'dev' })
|
|
127
|
+
enginesToDetach.push(engine)
|
|
128
|
+
return engine
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const mount = (tag: string): HTMLElement => {
|
|
132
|
+
const element = document.createElement(tag)
|
|
133
|
+
document.body.appendChild(element)
|
|
134
|
+
return element
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dispatches the way a MicroDot does: ON the emitting element, bubbling up to
|
|
139
|
+
* the engine's `document` listener, so `event.target` names the source exactly
|
|
140
|
+
* as `defineMicroDot` produces it
|
|
141
|
+
* (`packages/microdots-element/src/defineMicroDot.ts:281-283`). A wire's
|
|
142
|
+
* `from` is matched against that tag, so every test that expects a write must
|
|
143
|
+
* emit from the tag its wire names.
|
|
144
|
+
*/
|
|
145
|
+
const dispatch = (sourceTag: string, name: string, detail: unknown): void => {
|
|
146
|
+
const existing = document.querySelector(sourceTag)
|
|
147
|
+
const source = existing ?? mount(sourceTag)
|
|
148
|
+
source.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* An event with NO identifiable emitter: dispatched straight at `document`,
|
|
153
|
+
* whose `target` is a Document and not an Element. Nothing in this repo emits
|
|
154
|
+
* this way — it stands for the shapes that reach a `document` listener anyway
|
|
155
|
+
* (a foreign page's synthetic event, a retargeted non-element node).
|
|
156
|
+
*/
|
|
157
|
+
const dispatchAnonymously = (name: string, detail: unknown): void => {
|
|
158
|
+
document.dispatchEvent(new CustomEvent(name, { detail }))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The wire ids a trace list carries, in order — watch taps excluded. */
|
|
162
|
+
const firedWireIds = (
|
|
163
|
+
traces: ReadonlyArray<WireTrace>,
|
|
164
|
+
): ReadonlyArray<string> =>
|
|
165
|
+
traces.flatMap(trace => (trace._tag === 'wire' ? [trace.wireId] : []))
|
|
166
|
+
|
|
167
|
+
/** The payloads the watch taps logged, in order — wire traces excluded. */
|
|
168
|
+
const tappedPayloads = (
|
|
169
|
+
traces: ReadonlyArray<WireTrace>,
|
|
170
|
+
): ReadonlyArray<unknown> =>
|
|
171
|
+
traces.flatMap(trace => (trace._tag === 'watch' ? [trace.payload] : []))
|
|
172
|
+
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
document.body.replaceChildren()
|
|
175
|
+
enginesToDetach.splice(0).forEach(engine => {
|
|
176
|
+
engine.detach()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// The failure mode this whole file guards: the hand-written listener blocks
|
|
181
|
+
// in apps/host/src/entry.ts are being REPLACED by these records, and any
|
|
182
|
+
// drift between what they did and what the engine does is a silently broken
|
|
183
|
+
// demo — the events still fire, the log still scrolls, and nothing arrives.
|
|
184
|
+
describe('attachWireEngine', () => {
|
|
185
|
+
test('routes EFCT to eu-west through the w1 lookup rows', () => {
|
|
186
|
+
const fleet = mount('fleet-health')
|
|
187
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
188
|
+
|
|
189
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
190
|
+
|
|
191
|
+
expect(fleet.getAttribute('region')).toBe('eu-west')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('falls back to us-east for a symbol outside the lookup rows', () => {
|
|
195
|
+
const fleet = mount('fleet-health')
|
|
196
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
197
|
+
|
|
198
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'ZZZZ', cents: 1 })
|
|
199
|
+
|
|
200
|
+
expect(fleet.getAttribute('region')).toBe('us-east')
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('a disabled wire writes nothing and traces the skip', () => {
|
|
204
|
+
const fleet = mount('fleet-health')
|
|
205
|
+
const traces: Array<WireTrace> = []
|
|
206
|
+
const engine = attach({
|
|
207
|
+
topology: { wires: DEMO_WIRES, watch: [] },
|
|
208
|
+
onTrace: trace => traces.push(trace),
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
engine.setEnabled('w1', false)
|
|
212
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
213
|
+
|
|
214
|
+
expect(fleet.getAttribute('region')).toBeNull()
|
|
215
|
+
expect(traces).toEqual([
|
|
216
|
+
{
|
|
217
|
+
_tag: 'wire',
|
|
218
|
+
event: 'quote-changed',
|
|
219
|
+
wireId: 'w1',
|
|
220
|
+
delivery: { _tag: 'skipped', reason: 'disabled' },
|
|
221
|
+
},
|
|
222
|
+
])
|
|
223
|
+
|
|
224
|
+
// The kill switch is not a detach: flipping it back restores the wire.
|
|
225
|
+
engine.setEnabled('w1', true)
|
|
226
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
227
|
+
expect(fleet.getAttribute('region')).toBe('eu-west')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('encodes the numeric version 7 as the attribute string "7"', () => {
|
|
231
|
+
const form = mount('bespoke-contact-form')
|
|
232
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
233
|
+
|
|
234
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', { version: 7 })
|
|
235
|
+
|
|
236
|
+
expect(form.getAttribute('definition-token')).toBe('7')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* `setAttribute` fires `attributeChangedCallback` even when the value is
|
|
241
|
+
* unchanged, so two identical events must reach the element as ONE write —
|
|
242
|
+
* the guard lives in `setMicroDotAttribute` and the engine must go through
|
|
243
|
+
* it (apps/host/src/loader.test.ts pins the same guard directly).
|
|
244
|
+
*/
|
|
245
|
+
test('two identical events produce exactly one setAttribute call', () => {
|
|
246
|
+
const form = mount('bespoke-contact-form')
|
|
247
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
248
|
+
|
|
249
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', { version: 7 })
|
|
250
|
+
expect(form.getAttribute('definition-token')).toBe('7')
|
|
251
|
+
|
|
252
|
+
const spy = vi.spyOn(form, 'setAttribute')
|
|
253
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', { version: 7 })
|
|
254
|
+
|
|
255
|
+
expect(spy).not.toHaveBeenCalled()
|
|
256
|
+
spy.mockRestore()
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('falls back for a lookup key inherited from Object.prototype', () => {
|
|
260
|
+
// 'constructor' is not in w1's rows, but a bare `rows[encoded]` index
|
|
261
|
+
// would find Object.prototype.constructor and write
|
|
262
|
+
// 'function Object() { [native code] }' where the fallback belongs.
|
|
263
|
+
const fleet = mount('fleet-health')
|
|
264
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
265
|
+
|
|
266
|
+
dispatch('price-ticker', 'quote-changed', {
|
|
267
|
+
symbol: 'constructor',
|
|
268
|
+
cents: 1,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
expect(fleet.getAttribute('region')).toBe('us-east')
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('a payload that is not a record skips as no-field', () => {
|
|
275
|
+
const form = mount('bespoke-contact-form')
|
|
276
|
+
const traces: Array<WireTrace> = []
|
|
277
|
+
attach({
|
|
278
|
+
topology: { wires: DEMO_WIRES, watch: [] },
|
|
279
|
+
onTrace: trace => traces.push(trace),
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', 'seven')
|
|
283
|
+
|
|
284
|
+
expect(form.getAttribute('definition-token')).toBeNull()
|
|
285
|
+
expect(traces).toEqual([
|
|
286
|
+
{
|
|
287
|
+
_tag: 'wire',
|
|
288
|
+
event: 'definition-changed',
|
|
289
|
+
wireId: 'w3',
|
|
290
|
+
delivery: { _tag: 'skipped', reason: 'no-field' },
|
|
291
|
+
},
|
|
292
|
+
])
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test("a payload missing the wire's field skips as no-field", () => {
|
|
296
|
+
const form = mount('bespoke-contact-form')
|
|
297
|
+
const traces: Array<WireTrace> = []
|
|
298
|
+
attach({
|
|
299
|
+
topology: { wires: DEMO_WIRES, watch: [] },
|
|
300
|
+
onTrace: trace => traces.push(trace),
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', { revision: 7 })
|
|
304
|
+
|
|
305
|
+
expect(form.getAttribute('definition-token')).toBeNull()
|
|
306
|
+
expect(traces).toEqual([
|
|
307
|
+
{
|
|
308
|
+
_tag: 'wire',
|
|
309
|
+
event: 'definition-changed',
|
|
310
|
+
wireId: 'w3',
|
|
311
|
+
delivery: { _tag: 'skipped', reason: 'no-field' },
|
|
312
|
+
},
|
|
313
|
+
])
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('a value that fails its declared fieldType decode skips as no-field', () => {
|
|
317
|
+
const form = mount('bespoke-contact-form')
|
|
318
|
+
const traces: Array<WireTrace> = []
|
|
319
|
+
attach({
|
|
320
|
+
topology: { wires: DEMO_WIRES, watch: [] },
|
|
321
|
+
onTrace: trace => traces.push(trace),
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
dispatch('bespoke-contact-dashboard', 'definition-changed', {
|
|
325
|
+
version: 'seven',
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
expect(form.getAttribute('definition-token')).toBeNull()
|
|
329
|
+
expect(traces).toEqual([
|
|
330
|
+
{
|
|
331
|
+
_tag: 'wire',
|
|
332
|
+
event: 'definition-changed',
|
|
333
|
+
wireId: 'w3',
|
|
334
|
+
delivery: { _tag: 'skipped', reason: 'no-field' },
|
|
335
|
+
},
|
|
336
|
+
])
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('a json field writes its JSON.stringify encoding', () => {
|
|
340
|
+
const fleet = mount('fleet-health')
|
|
341
|
+
attach({ topology: { wires: [wJson], watch: [] } })
|
|
342
|
+
|
|
343
|
+
dispatch('bespoke-contact-dashboard', 'snapshot-changed', {
|
|
344
|
+
snapshot: { id: 7, tags: ['a'] },
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
expect(fleet.getAttribute('snapshot')).toBe('{"id":7,"tags":["a"]}')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('an unencodable json value skips its wire without killing siblings on the same event', () => {
|
|
351
|
+
// A postMessage structured clone legally carries cycles, and
|
|
352
|
+
// JSON.stringify THROWS on them — an uncaught throw inside the listener
|
|
353
|
+
// would drop the sibling's write and trace too.
|
|
354
|
+
const fleet = mount('fleet-health')
|
|
355
|
+
const sibling: Wire = {
|
|
356
|
+
...wJson,
|
|
357
|
+
id: 'w-sibling',
|
|
358
|
+
field: 'label',
|
|
359
|
+
fieldType: 'string',
|
|
360
|
+
input: 'label',
|
|
361
|
+
inputType: 'string',
|
|
362
|
+
}
|
|
363
|
+
const traces: Array<WireTrace> = []
|
|
364
|
+
attach({
|
|
365
|
+
topology: { wires: [wJson, sibling], watch: [] },
|
|
366
|
+
onTrace: trace => traces.push(trace),
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
const cyclic: Record<string, unknown> = {}
|
|
370
|
+
cyclic['self'] = cyclic
|
|
371
|
+
dispatch('bespoke-contact-dashboard', 'snapshot-changed', {
|
|
372
|
+
snapshot: cyclic,
|
|
373
|
+
label: 'ok',
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
expect(fleet.getAttribute('snapshot')).toBeNull()
|
|
377
|
+
expect(fleet.getAttribute('label')).toBe('ok')
|
|
378
|
+
expect(traces).toEqual([
|
|
379
|
+
{
|
|
380
|
+
_tag: 'wire',
|
|
381
|
+
event: 'snapshot-changed',
|
|
382
|
+
wireId: 'w-json',
|
|
383
|
+
delivery: { _tag: 'skipped', reason: 'no-field' },
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
_tag: 'wire',
|
|
387
|
+
event: 'snapshot-changed',
|
|
388
|
+
wireId: 'w-sibling',
|
|
389
|
+
delivery: {
|
|
390
|
+
_tag: 'written',
|
|
391
|
+
write: { toTag: 'fleet-health', input: 'label', value: 'ok' },
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
])
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
test('every mounted instance of the target tag receives the write', () => {
|
|
398
|
+
// A topology may place one tag in two slots; "the first in document
|
|
399
|
+
// order" is not what the record says, and 'written' must mean all of them.
|
|
400
|
+
const first = mount('fleet-health')
|
|
401
|
+
const second = mount('fleet-health')
|
|
402
|
+
attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
403
|
+
|
|
404
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
405
|
+
|
|
406
|
+
expect(first.getAttribute('region')).toBe('eu-west')
|
|
407
|
+
expect(second.getAttribute('region')).toBe('eu-west')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('a wire whose `to` is not a valid selector is target-unmounted, and later wires still trace', () => {
|
|
411
|
+
// The schema admits any string as `to`; querySelectorAll('') throws, and
|
|
412
|
+
// an uncaught throw would abort trace emission for every remaining wire.
|
|
413
|
+
const fleet = mount('fleet-health')
|
|
414
|
+
const malformed: Wire = { ...w1, id: 'w-bad', to: '' }
|
|
415
|
+
const traces: Array<WireTrace> = []
|
|
416
|
+
attach({
|
|
417
|
+
topology: { wires: [malformed, w1], watch: [] },
|
|
418
|
+
onTrace: trace => traces.push(trace),
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
422
|
+
|
|
423
|
+
expect(fleet.getAttribute('region')).toBe('eu-west')
|
|
424
|
+
expect(traces).toEqual([
|
|
425
|
+
{
|
|
426
|
+
_tag: 'wire',
|
|
427
|
+
event: 'quote-changed',
|
|
428
|
+
wireId: 'w-bad',
|
|
429
|
+
delivery: {
|
|
430
|
+
_tag: 'target-unmounted',
|
|
431
|
+
write: { toTag: '', input: 'region', value: 'eu-west' },
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
_tag: 'wire',
|
|
436
|
+
event: 'quote-changed',
|
|
437
|
+
wireId: 'w1',
|
|
438
|
+
delivery: {
|
|
439
|
+
_tag: 'written',
|
|
440
|
+
write: { toTag: 'fleet-health', input: 'region', value: 'eu-west' },
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
])
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
test('a wire whose `input` is not a valid attribute name fails its write, and siblings on the same event still land', () => {
|
|
447
|
+
// The schema admits any string as `input`; setAttribute('refresh token')
|
|
448
|
+
// throws InvalidCharacterError, and an uncaught throw out of the document
|
|
449
|
+
// listener would drop the write AND trace of every remaining wire.
|
|
450
|
+
const fleet = mount('fleet-health')
|
|
451
|
+
const hostile: Wire = { ...w1, id: 'w-hostile', input: 'refresh token' }
|
|
452
|
+
const traces: Array<WireTrace> = []
|
|
453
|
+
attach({
|
|
454
|
+
topology: { wires: [hostile, w1], watch: [] },
|
|
455
|
+
onTrace: trace => traces.push(trace),
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
459
|
+
|
|
460
|
+
expect(fleet.getAttribute('region')).toBe('eu-west')
|
|
461
|
+
expect(traces).toEqual([
|
|
462
|
+
{
|
|
463
|
+
_tag: 'wire',
|
|
464
|
+
event: 'quote-changed',
|
|
465
|
+
wireId: 'w-hostile',
|
|
466
|
+
delivery: {
|
|
467
|
+
_tag: 'write-failed',
|
|
468
|
+
write: {
|
|
469
|
+
toTag: 'fleet-health',
|
|
470
|
+
input: 'refresh token',
|
|
471
|
+
value: 'eu-west',
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
_tag: 'wire',
|
|
477
|
+
event: 'quote-changed',
|
|
478
|
+
wireId: 'w1',
|
|
479
|
+
delivery: {
|
|
480
|
+
_tag: 'written',
|
|
481
|
+
write: { toTag: 'fleet-health', input: 'region', value: 'eu-west' },
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
])
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
test('the watch tap traces before the wire delivery when an event is both watched and wired', () => {
|
|
488
|
+
// Today's hosts log before they act; a wire's write must never become a
|
|
489
|
+
// precondition of the log line.
|
|
490
|
+
mount('fleet-health')
|
|
491
|
+
const traces: Array<WireTrace> = []
|
|
492
|
+
attach({
|
|
493
|
+
topology: { wires: DEMO_WIRES, watch: [{ event: 'quote-changed' }] },
|
|
494
|
+
onTrace: trace => traces.push(trace),
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
498
|
+
|
|
499
|
+
expect(traces.map(trace => trace._tag)).toEqual(['watch', 'wire'])
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
test('a held condition writes nothing and the trace names the wanted value', () => {
|
|
503
|
+
const fleet = mount('fleet-health')
|
|
504
|
+
const conditioned: Wire = {
|
|
505
|
+
...w1,
|
|
506
|
+
id: 'w-linked',
|
|
507
|
+
transform: {
|
|
508
|
+
_tag: 'condition',
|
|
509
|
+
field: 'linked',
|
|
510
|
+
op: 'is',
|
|
511
|
+
value: 'true',
|
|
512
|
+
},
|
|
513
|
+
}
|
|
514
|
+
const traces: Array<WireTrace> = []
|
|
515
|
+
attach({
|
|
516
|
+
topology: { wires: [conditioned], watch: [] },
|
|
517
|
+
onTrace: trace => traces.push(trace),
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', linked: false })
|
|
521
|
+
|
|
522
|
+
expect(fleet.getAttribute('region')).toBeNull()
|
|
523
|
+
expect(traces).toEqual([
|
|
524
|
+
{
|
|
525
|
+
_tag: 'wire',
|
|
526
|
+
event: 'quote-changed',
|
|
527
|
+
wireId: 'w-linked',
|
|
528
|
+
delivery: { _tag: 'held', wanted: 'true' },
|
|
529
|
+
},
|
|
530
|
+
])
|
|
531
|
+
|
|
532
|
+
// The condition holding is the whole difference: same event, one write.
|
|
533
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', linked: true })
|
|
534
|
+
expect(fleet.getAttribute('region')).toBe('EFCT')
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
test('a watch tap traces the event name and raw payload without writing', () => {
|
|
538
|
+
const traces: Array<WireTrace> = []
|
|
539
|
+
attach({
|
|
540
|
+
topology: { wires: [], watch: [{ event: 'progress-changed' }] },
|
|
541
|
+
onTrace: trace => traces.push(trace),
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
dispatch('cumulative-functional-spec-generator-panel', 'progress-changed', {
|
|
545
|
+
current: 3,
|
|
546
|
+
total: 9,
|
|
547
|
+
stage: 'drafting',
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
expect(traces).toEqual([
|
|
551
|
+
{
|
|
552
|
+
_tag: 'watch',
|
|
553
|
+
event: 'progress-changed',
|
|
554
|
+
payload: { current: 3, total: 9, stage: 'drafting' },
|
|
555
|
+
},
|
|
556
|
+
])
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
test('an unmounted target is a skipped write, still traced — the poll floor covers it', () => {
|
|
560
|
+
const traces: Array<WireTrace> = []
|
|
561
|
+
attach({
|
|
562
|
+
topology: { wires: DEMO_WIRES, watch: [] },
|
|
563
|
+
onTrace: trace => traces.push(trace),
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
dispatch('bespoke-contact-form', 'submission-created', { id: 'sub-42' })
|
|
567
|
+
|
|
568
|
+
expect(traces).toEqual([
|
|
569
|
+
{
|
|
570
|
+
_tag: 'wire',
|
|
571
|
+
event: 'submission-created',
|
|
572
|
+
wireId: 'w2',
|
|
573
|
+
delivery: {
|
|
574
|
+
_tag: 'target-unmounted',
|
|
575
|
+
write: {
|
|
576
|
+
toTag: 'bespoke-contact-dashboard',
|
|
577
|
+
input: 'refresh-token',
|
|
578
|
+
value: 'sub-42',
|
|
579
|
+
},
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
])
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* The Phase 5 defect, pinned. Under a name-only match the Pages rail's host
|
|
587
|
+
* picker ALSO retargeted the hidden Wiring screen through pw12, and the
|
|
588
|
+
* Wiring canvas's picker drove the Pages panes through pg6/pg7 — panes
|
|
589
|
+
* resolving a host the rail's picker was not showing, after which a route
|
|
590
|
+
* click sent paths from host A into panes rendering host B. All four wires
|
|
591
|
+
* derive `live`, so the Wiring screen showed nothing wrong.
|
|
592
|
+
*/
|
|
593
|
+
test('two dots emitting the same event name drive only the wire whose `from` matches', () => {
|
|
594
|
+
const table = mount('wiring-table')
|
|
595
|
+
const canvas = mount('pages-canvas')
|
|
596
|
+
const inspector = mount('pages-inspector')
|
|
597
|
+
const traces: Array<WireTrace> = []
|
|
598
|
+
attach({
|
|
599
|
+
topology: { wires: [pw12, pg6, pg7], watch: [] },
|
|
600
|
+
onTrace: trace => traces.push(trace),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
dispatch('pages-rail', 'host-changed', { hostId: 'platform-host' })
|
|
604
|
+
|
|
605
|
+
expect(canvas.getAttribute('host-id')).toBe('platform-host')
|
|
606
|
+
expect(inspector.getAttribute('host-id')).toBe('platform-host')
|
|
607
|
+
// The other screen is untouched — the whole defect in one assertion.
|
|
608
|
+
expect(table.getAttribute('host-id')).toBeNull()
|
|
609
|
+
// …and pw12 does not even appear in the trace: this is not its event.
|
|
610
|
+
expect(firedWireIds(traces)).toEqual(['pg6', 'pg7'])
|
|
611
|
+
|
|
612
|
+
dispatch('wiring-canvas', 'host-changed', { hostId: 'demo-host' })
|
|
613
|
+
|
|
614
|
+
expect(table.getAttribute('host-id')).toBe('demo-host')
|
|
615
|
+
// The Pages panes keep the host their own rail last chose.
|
|
616
|
+
expect(canvas.getAttribute('host-id')).toBe('platform-host')
|
|
617
|
+
expect(inspector.getAttribute('host-id')).toBe('platform-host')
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
test('an event with no identifiable source fires no wires and traces the source skip', () => {
|
|
621
|
+
const canvas = mount('pages-canvas')
|
|
622
|
+
const traces: Array<WireTrace> = []
|
|
623
|
+
attach({
|
|
624
|
+
topology: { wires: [pw12, pg6], watch: [] },
|
|
625
|
+
onTrace: trace => traces.push(trace),
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
dispatchAnonymously('host-changed', { hostId: 'platform-host' })
|
|
629
|
+
|
|
630
|
+
expect(canvas.getAttribute('host-id')).toBeNull()
|
|
631
|
+
expect(traces).toEqual([
|
|
632
|
+
{
|
|
633
|
+
_tag: 'wire',
|
|
634
|
+
event: 'host-changed',
|
|
635
|
+
wireId: 'pw12',
|
|
636
|
+
delivery: { _tag: 'skipped', reason: 'source' },
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
_tag: 'wire',
|
|
640
|
+
event: 'host-changed',
|
|
641
|
+
wireId: 'pg6',
|
|
642
|
+
delivery: { _tag: 'skipped', reason: 'source' },
|
|
643
|
+
},
|
|
644
|
+
])
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
test('a watch tap is the host log, not a wire: it fires for every emitter and for none', () => {
|
|
648
|
+
// The tap is keyed on the event NAME alone, deliberately — a log that
|
|
649
|
+
// only recorded the one dot a wire happens to name would stop being a
|
|
650
|
+
// log. Three emitters of `host-changed`, three lines.
|
|
651
|
+
const traces: Array<WireTrace> = []
|
|
652
|
+
attach({
|
|
653
|
+
topology: { wires: [pw12], watch: [{ event: 'host-changed' }] },
|
|
654
|
+
onTrace: trace => traces.push(trace),
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
dispatch('pages-rail', 'host-changed', { hostId: 'a' })
|
|
658
|
+
dispatch('wiring-canvas', 'host-changed', { hostId: 'b' })
|
|
659
|
+
dispatchAnonymously('host-changed', { hostId: 'c' })
|
|
660
|
+
|
|
661
|
+
expect(tappedPayloads(traces)).toEqual([
|
|
662
|
+
{ hostId: 'a' },
|
|
663
|
+
{ hostId: 'b' },
|
|
664
|
+
{ hostId: 'c' },
|
|
665
|
+
])
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
test('detach removes every listener', () => {
|
|
669
|
+
const fleet = mount('fleet-health')
|
|
670
|
+
const engine = attach({ topology: { wires: DEMO_WIRES, watch: [] } })
|
|
671
|
+
|
|
672
|
+
engine.detach()
|
|
673
|
+
dispatch('price-ticker', 'quote-changed', { symbol: 'EFCT', cents: 12345 })
|
|
674
|
+
|
|
675
|
+
expect(fleet.getAttribute('region')).toBeNull()
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
// The failure mode: a host's immediate-apply path (the demo host's link
|
|
680
|
+
// toggle) re-applies a wire's transform OUTSIDE the engine, and a second
|
|
681
|
+
// implementation of the rows/fallback semantics would drift from what the
|
|
682
|
+
// engine writes when the event actually fires.
|
|
683
|
+
describe('applyTransform', () => {
|
|
684
|
+
test('agrees with planWires on rows, fallback, and prototype-key misses', () => {
|
|
685
|
+
const symbols = ['EFCT', 'ZZZZ', 'constructor']
|
|
686
|
+
symbols.forEach(symbol => {
|
|
687
|
+
const plan = planWires(
|
|
688
|
+
[w1],
|
|
689
|
+
{
|
|
690
|
+
name: 'quote-changed',
|
|
691
|
+
sourceTag: Option.some(w1.from),
|
|
692
|
+
payload: { symbol, cents: 1 },
|
|
693
|
+
},
|
|
694
|
+
{ env: 'dev', isEnabled: () => true },
|
|
695
|
+
)
|
|
696
|
+
expect(
|
|
697
|
+
plan.writes.map(write => write.value),
|
|
698
|
+
symbol,
|
|
699
|
+
).toEqual([applyTransform(w1.transform, symbol)])
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
test('direct and condition pass the encoded value straight through', () => {
|
|
704
|
+
expect(applyTransform({ _tag: 'direct' }, 'sub-42')).toBe('sub-42')
|
|
705
|
+
expect(
|
|
706
|
+
applyTransform(
|
|
707
|
+
{ _tag: 'condition', field: 'linked', op: 'is', value: 'true' },
|
|
708
|
+
'EFCT',
|
|
709
|
+
),
|
|
710
|
+
).toBe('EFCT')
|
|
711
|
+
})
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
// The failure mode: a planner that quietly grew a DOM dependency would make
|
|
715
|
+
// every non-DOM transport (the cross-origin bridge, a future server-side
|
|
716
|
+
// derivation) impossible — transport-blindness is property 2 of
|
|
717
|
+
// wiki/framework/composition/wire-execution-and-transforms.md.
|
|
718
|
+
describe('planWires', () => {
|
|
719
|
+
test('plans a bridge postMessage envelope with no document and no DOM event', () => {
|
|
720
|
+
// Shaped like apps/cross-origin-demo/src/bridge.ts's SubmissionCreated —
|
|
721
|
+
// a postMessage envelope, not a CustomEvent detail. The planner neither
|
|
722
|
+
// knows nor cares HOW the source was identified, only that it was: with
|
|
723
|
+
// no `event.target` to read, a non-DOM transport names the emitting tag
|
|
724
|
+
// in the envelope itself.
|
|
725
|
+
const envelope = {
|
|
726
|
+
version: 1,
|
|
727
|
+
type: 'submission-created',
|
|
728
|
+
from: 'bespoke-contact-form',
|
|
729
|
+
submissionId: 'sub-42',
|
|
730
|
+
}
|
|
731
|
+
const bridgeWire: Wire = { ...w2, field: 'submissionId' }
|
|
732
|
+
|
|
733
|
+
const plan = planWires(
|
|
734
|
+
[bridgeWire],
|
|
735
|
+
{
|
|
736
|
+
name: envelope.type,
|
|
737
|
+
sourceTag: Option.some(envelope.from),
|
|
738
|
+
payload: envelope,
|
|
739
|
+
},
|
|
740
|
+
{ env: 'prod', isEnabled: () => true },
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
expect(plan.writes).toEqual([
|
|
744
|
+
{
|
|
745
|
+
toTag: 'bespoke-contact-dashboard',
|
|
746
|
+
input: 'refresh-token',
|
|
747
|
+
value: 'sub-42',
|
|
748
|
+
},
|
|
749
|
+
])
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
test('an envelope naming a different sender matches nothing and traces nothing', () => {
|
|
753
|
+
// The same name, the same payload shape, a different dot. Not a skip:
|
|
754
|
+
// this event is not this wire's event at all, and a trace line for it
|
|
755
|
+
// would be noise in every host log that carries a shared event name.
|
|
756
|
+
const bridgeWire: Wire = { ...w2, field: 'submissionId' }
|
|
757
|
+
|
|
758
|
+
const plan = planWires(
|
|
759
|
+
[bridgeWire],
|
|
760
|
+
{
|
|
761
|
+
name: 'submission-created',
|
|
762
|
+
sourceTag: Option.some('some-other-form'),
|
|
763
|
+
payload: { submissionId: 'sub-42' },
|
|
764
|
+
},
|
|
765
|
+
{ env: 'prod', isEnabled: () => true },
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
expect(plan).toEqual({ writes: [], outcomes: [] })
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
test('a source the transport could not identify skips every name-matched wire', () => {
|
|
772
|
+
// `None` is not a wildcard: a wire whose source cannot be confirmed must
|
|
773
|
+
// NOT fire, because "some dot emitted host-changed" is exactly the
|
|
774
|
+
// ambiguity the `from` match exists to remove. It still traces — a
|
|
775
|
+
// name-matched wire going quiet is what a trace has to be able to show.
|
|
776
|
+
const plan = planWires(
|
|
777
|
+
[w2, w3],
|
|
778
|
+
{
|
|
779
|
+
name: 'submission-created',
|
|
780
|
+
sourceTag: Option.none(),
|
|
781
|
+
payload: { id: 'sub-1' },
|
|
782
|
+
},
|
|
783
|
+
{ env: 'prod', isEnabled: () => true },
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
expect(plan.writes).toEqual([])
|
|
787
|
+
expect(plan.outcomes).toEqual([
|
|
788
|
+
{ _tag: 'skipped', wireId: 'w2', reason: 'source' },
|
|
789
|
+
])
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
test('skips a wire whose envs exclude the running environment, distinctly from disabled', () => {
|
|
793
|
+
const devOnly: Wire = { ...w2, envs: ['dev'] }
|
|
794
|
+
|
|
795
|
+
const plan = planWires(
|
|
796
|
+
[devOnly],
|
|
797
|
+
{
|
|
798
|
+
name: 'submission-created',
|
|
799
|
+
sourceTag: Option.some('bespoke-contact-form'),
|
|
800
|
+
payload: { id: 'sub-1' },
|
|
801
|
+
},
|
|
802
|
+
{ env: 'prod', isEnabled: () => true },
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
expect(plan.writes).toEqual([])
|
|
806
|
+
expect(plan.outcomes).toEqual([
|
|
807
|
+
{ _tag: 'skipped', wireId: 'w2', reason: 'env' },
|
|
808
|
+
])
|
|
809
|
+
})
|
|
810
|
+
})
|