@toa.io/norm 1.0.0-alpha.32 → 1.0.0-alpha.320

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/package.json +9 -7
  3. package/src/.component/.expand/bridge.js +2 -6
  4. package/src/.component/.expand/entity.js +2 -9
  5. package/src/.component/.expand/events.js +2 -6
  6. package/src/.component/.expand/extensions.js +7 -9
  7. package/src/.component/.expand/index.js +7 -19
  8. package/src/.component/.expand/operations.js +2 -9
  9. package/src/.component/.expand/receivers.js +4 -7
  10. package/src/.component/.expand/version.js +69 -28
  11. package/src/.component/.normalize/entity.js +7 -0
  12. package/src/.component/.normalize/events.js +16 -4
  13. package/src/.component/.normalize/index.js +4 -9
  14. package/src/.component/.normalize/operations.js +13 -6
  15. package/src/.component/.normalize/receivers.js +4 -5
  16. package/src/.component/collapse.js +60 -9
  17. package/src/.component/defaults.js +5 -11
  18. package/src/.component/dependencies.js +14 -10
  19. package/src/.component/dereference.js +5 -49
  20. package/src/.component/expand.js +3 -9
  21. package/src/.component/extensions.js +9 -10
  22. package/src/.component/index.js +10 -21
  23. package/src/.component/merge.js +25 -15
  24. package/src/.component/migrations.js +57 -0
  25. package/src/.component/normalize.js +10 -6
  26. package/src/.component/schema.yaml +162 -73
  27. package/src/.component/validate.js +63 -17
  28. package/src/.context/.dependencies/connectors.js +5 -9
  29. package/src/.context/.dependencies/describe.js +4 -5
  30. package/src/.context/.dependencies/extensions.js +93 -20
  31. package/src/.context/.dependencies/index.js +3 -9
  32. package/src/.context/.dependencies/resolve.js +44 -7
  33. package/src/.context/complete.js +1 -5
  34. package/src/.context/converge.js +31 -0
  35. package/src/.context/dependencies.js +43 -6
  36. package/src/.context/dereference.js +11 -6
  37. package/src/.context/evict.js +38 -0
  38. package/src/.context/expand.js +11 -6
  39. package/src/.context/index.js +8 -15
  40. package/src/.context/normalize.js +9 -7
  41. package/src/.context/schema.yaml +183 -11
  42. package/src/.context/validate.js +11 -14
  43. package/src/component.js +33 -14
  44. package/src/context.js +44 -15
  45. package/src/definition.js +63 -0
  46. package/src/entity.js +15 -0
  47. package/src/index.js +7 -9
  48. package/src/manifest.js +154 -0
  49. package/src/map.js +24 -0
  50. package/src/shortcuts.js +11 -15
  51. package/test/component/collapse.fixtures.js +30 -39
  52. package/test/component/collapse.test.js +90 -10
  53. package/test/component/dependencies.test.js +16 -14
  54. package/test/component/dereference.fixtures.js +10 -98
  55. package/test/component/dereference.test.js +10 -14
  56. package/test/component/dummies/extension/index.js +1 -5
  57. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yaml +2 -0
  58. package/test/component/dummies/migrations/conflicting/migrations/0001-one.yml +2 -0
  59. package/test/component/dummies/migrations/malformed/migrations/0001-one.yaml +1 -0
  60. package/test/component/dummies/migrations/ordered/migrations/0001-first.yml +4 -0
  61. package/test/component/dummies/migrations/ordered/migrations/0002-second.yaml +4 -0
  62. package/test/component/dummies/migrations/ordered/migrations/0010-third.json +1 -0
  63. package/test/component/dummies/migrations/stateless/migrations/0001-one.yaml +2 -0
  64. package/test/component/expand.fixtures.js +13 -13
  65. package/test/component/expand.test.js +17 -13
  66. package/test/component/migrations.test.js +62 -0
  67. package/test/component/normalize.fixtures.js +3 -7
  68. package/test/component/normalize.test.js +94 -28
  69. package/test/component/validate.fixtures.js +17 -15
  70. package/test/component/validate.test.js +271 -109
  71. package/test/context/complete.fixtures.js +2 -8
  72. package/test/context/complete.test.js +14 -8
  73. package/test/context/converge.test.js +57 -0
  74. package/test/context/dereference.fixtures.js +3 -9
  75. package/test/context/dereference.test.js +18 -7
  76. package/test/context/evict.fixtures.js +39 -0
  77. package/test/context/evict.test.js +73 -0
  78. package/test/context/expand.fixtures.js +1 -5
  79. package/test/context/expand.test.js +36 -8
  80. package/test/context/normalize.fixtures.js +2 -7
  81. package/test/context/normalize.test.js +33 -9
  82. package/test/context/validate.fixtures.js +1 -6
  83. package/test/context/validate.test.js +173 -35
  84. package/test/definition.test.js +35 -0
  85. package/test/dummies/bare/index.js +1 -0
  86. package/test/dummies/defined/definition.js +1 -0
  87. package/test/dummies/defined/index.js +1 -0
  88. package/test/dummies/defined/package.json +1 -0
  89. package/test/manifest.test.js +126 -0
  90. package/test/shortcuts.fixtures.js +4 -9
  91. package/test/shortcuts.test.js +17 -15
  92. package/test/system.test.js +31 -0
  93. package/types/component.d.ts +29 -4
  94. package/types/context/declaration.d.ts +34 -3
  95. package/types/context.d.ts +27 -13
  96. package/types/entity.d.ts +7 -0
  97. package/types/index.d.ts +18 -3
  98. package/src/.component/.expand/properties.js +0 -14
  99. package/src/.context/.dependencies/load.js +0 -36
  100. package/test/context/dependencies.load.test.js +0 -24
@@ -1,11 +1,10 @@
1
- // noinspection JSUnresolvedVariable
1
+ import { describe, it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
2
3
 
3
- 'use strict'
4
+ import clone from 'clone-deep'
4
5
 
5
- const clone = require('clone-deep')
6
-
7
- const { validate } = require('../../src/.component')
8
- const fixtures = require('./validate.fixtures')
6
+ import { validate } from '../../src/.component/index.js'
7
+ import * as fixtures from './validate.fixtures.js'
9
8
 
10
9
  let manifest
11
10
 
@@ -13,249 +12,412 @@ beforeEach(() => {
13
12
  manifest = clone(fixtures.ok)
14
13
  })
15
14
 
16
- it('should be ok', () => {
17
- expect(() => validate(manifest)).not.toThrow()
15
+ it('should be ok', async () => {
16
+ await assert.doesNotReject(validate(manifest))
18
17
  })
19
18
 
20
- it('should provide error', () => {
19
+ it('should provide error', async () => {
21
20
  manifest.foo = 'bar'
22
21
 
23
- expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
22
+ await assert.rejects(validate(manifest))
24
23
  })
25
24
 
26
- it('should not have additional properties', () => {
25
+ it('should not have additional properties', async () => {
27
26
  manifest.foo = 'bar'
28
27
 
29
- expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
28
+ await assert.rejects(validate(manifest))
30
29
  })
31
30
 
32
31
  describe('namespace', () => {
33
- it('should match token pattern', () => {
32
+ it('should match token pattern', async () => {
34
33
  manifest.namespace = '1'
35
- expect(() => validate(manifest)).toThrow(/must match pattern/)
34
+ await assert.rejects(validate(manifest), (error) =>
35
+ /must match pattern/.test(error.message)
36
+ )
36
37
 
37
38
  manifest.namespace = 'foo_'
38
- expect(() => validate(manifest)).toThrow(/must match pattern/)
39
+ await assert.rejects(validate(manifest), (error) =>
40
+ /must match pattern/.test(error.message)
41
+ )
39
42
 
40
43
  manifest.namespace = 'foo_bar'
41
- expect(() => validate(manifest)).toThrow(/must match pattern/)
44
+ await assert.rejects(validate(manifest), (error) =>
45
+ /must match pattern/.test(error.message)
46
+ )
42
47
 
43
48
  manifest.namespace = 'foo-'
44
- expect(() => validate(manifest)).toThrow(/must match pattern/)
49
+ await assert.rejects(validate(manifest), (error) =>
50
+ /must match pattern/.test(error.message)
51
+ )
45
52
 
46
53
  manifest.namespace = 'foo-bar'
47
- expect(() => validate(manifest)).toThrow('must match pattern')
54
+ await assert.rejects(validate(manifest), (error) =>
55
+ /must match pattern/.test(error.message)
56
+ )
48
57
 
49
58
  manifest.namespace = 'FooBar12'
50
- expect(() => validate(manifest)).not.toThrow()
59
+ await assert.doesNotReject(validate(manifest))
51
60
  })
52
61
 
53
- it('should forbid \'system\' namespace', () => {
62
+ it("should forbid 'system' namespace", async () => {
54
63
  manifest.namespace = 'system'
55
- expect(() => validate(manifest)).toThrow(/must NOT be valid/)
56
- })
57
-
58
- it('should set `default` namespace', async () => {
59
- delete manifest.namespace
60
-
61
- expect(() => validate(manifest)).not.toThrow()
62
- expect(manifest.namespace).toStrictEqual('default')
64
+ await assert.rejects(validate(manifest), (error) =>
65
+ /must NOT be valid/.test(error.message)
66
+ )
63
67
  })
64
68
  })
65
69
 
66
70
  describe('name', () => {
67
- it('should be optional', () => {
71
+ it('should be optional', async () => {
68
72
  delete manifest.name
69
- expect(() => validate(manifest)).not.toThrow()
73
+ await assert.doesNotReject(validate(manifest))
70
74
  })
71
75
  })
72
76
 
73
77
  describe('entity', () => {
74
- it('should be optional', () => {
78
+ it('should be optional', async () => {
75
79
  delete manifest.entity
76
- expect(() => validate(manifest)).not.toThrow()
80
+ await assert.doesNotReject(validate(manifest))
77
81
  })
78
82
 
79
- it('should be object', () => {
83
+ it('should be object', async () => {
80
84
  manifest.entity = 'foo'
81
- expect(() => validate(manifest)).toThrow(/must be object/)
85
+ await assert.rejects(validate(manifest), (error) =>
86
+ /must be object/.test(error.message)
87
+ )
82
88
  })
83
89
 
84
- it('should not have additional properties', () => {
90
+ it('should not have additional properties', async () => {
85
91
  manifest.entity.foo = 'bar'
86
- expect(() => validate(manifest)).toThrow(/must NOT have additional property/)
92
+ await assert.rejects(validate(manifest))
87
93
  })
88
94
 
89
- describe('schema', () => {
90
- it('should be required', () => {
91
- delete manifest.entity.schema
92
- expect(() => validate(manifest)).toThrow(/must have required property/)
95
+ describe('properties', () => {
96
+ it('should be required', async () => {
97
+ delete manifest.entity.properties
98
+ await assert.rejects(validate(manifest))
93
99
  })
94
100
 
95
- it('should be JSON schema object', () => {
96
- manifest.entity.schema = { properties: { foo: 'bar' } }
97
- expect(() => validate(manifest)).toThrow()
98
- })
99
-
100
- it('should be JSON schema object of type object', () => {
101
- manifest.entity.schema = { type: 'integer' }
102
- expect(() => validate(manifest)).toThrow(/must be equal to constant 'object'/)
103
-
104
- manifest.entity.schema = {}
105
- validate(manifest)
106
- expect(manifest.entity.schema.type).toBe('object')
101
+ it('should be JSON schemas', async () => {
102
+ manifest.entity.properties.foo = 'bar'
103
+ await assert.rejects(validate(manifest))
107
104
  })
108
105
 
109
- it('should have property names matching token pattern', () => {
110
- manifest.entity.schema.properties._foo = { type: 'string' }
111
- expect(() => validate(manifest)).toThrow(/pattern/)
106
+ it('should have names matching token pattern', async () => {
107
+ manifest.entity.properties._foo = { type: 'string' }
108
+ await assert.rejects(validate(manifest), (error) => /pattern/.test(error.message))
112
109
  })
113
110
 
114
- it('should allow default id', () => {
115
- manifest.entity.schema.properties.id = {
111
+ it('should allow own id', async () => {
112
+ manifest.entity.properties.id = {
116
113
  type: 'string',
117
114
  pattern: '^[a-fA-F0-9]+$'
118
115
  }
119
- expect(() => validate(manifest)).not.toThrow()
116
+ await assert.doesNotReject(validate(manifest))
117
+ })
118
+
119
+ it('should not declare TRAILERS', async () => {
120
+ manifest.entity.properties.TRAILERS = { type: 'object' }
121
+ await assert.rejects(validate(manifest), (error) =>
122
+ /System property 'TRAILERS' cannot be declared/.test(error.message)
123
+ )
120
124
  })
121
125
  })
122
126
 
123
- describe('associated', () => {
124
- it('should provide default', () => {
125
- expect(() => validate(manifest)).not.toThrow()
126
- expect(manifest.entity.associated).toBe(false)
127
+ describe('required', () => {
128
+ it('should name declared properties', async () => {
129
+ manifest.entity.required = ['name']
130
+ await assert.doesNotReject(validate(manifest))
131
+
132
+ manifest.entity.required = ['nope']
133
+ await assert.rejects(validate(manifest), (error) =>
134
+ /requires property 'nope'/.test(error.message)
135
+ )
136
+ })
137
+ })
138
+
139
+ describe('blank', () => {
140
+ it('should name declared properties', async () => {
141
+ manifest.entity.blank = { name: 'whatever' }
142
+ await assert.doesNotReject(validate(manifest))
143
+
144
+ manifest.entity.blank = { nope: 1 }
145
+ await assert.rejects(validate(manifest), (error) =>
146
+ /names property 'nope'/.test(error.message)
147
+ )
148
+ })
149
+
150
+ it('should not name a system property', async () => {
151
+ manifest.entity.blank = { VERSION: 1 }
152
+ await assert.rejects(validate(manifest), (error) =>
153
+ /must NOT be valid/.test(error.message)
154
+ )
155
+ })
156
+
157
+ it('should not name TRAILERS', async () => {
158
+ manifest.entity.blank = { TRAILERS: {} }
159
+ await assert.rejects(validate(manifest), (error) =>
160
+ /must NOT be valid/.test(error.message)
161
+ )
127
162
  })
128
163
  })
129
164
  })
130
165
 
131
166
  describe('bindings', () => {
132
- it('should be array of unique strings', () => {
167
+ it('should be array of unique strings', async () => {
133
168
  manifest.bindings = 'oops'
134
- expect(() => validate(manifest)).toThrow(/must be array/)
169
+ await assert.rejects(validate(manifest), (error) =>
170
+ /must be array/.test(error.message)
171
+ )
135
172
 
136
173
  manifest.bindings = ['oops', 'oops']
137
- expect(() => validate(manifest)).toThrow(/duplicate items/)
174
+ await assert.rejects(validate(manifest), (error) =>
175
+ /duplicate items/.test(error.message)
176
+ )
138
177
 
139
178
  manifest.bindings = ['oops', {}]
140
- expect(() => validate(manifest)).toThrow(/must be string/)
179
+ await assert.rejects(validate(manifest), (error) =>
180
+ /must be string/.test(error.message)
181
+ )
141
182
  })
142
183
 
143
- it('should forbid explicit loop', () => {
184
+ it('should forbid explicit loop', async () => {
144
185
  manifest.bindings = ['@toa.io/bindings.loop']
145
- expect(() => validate(manifest)).toThrow(/must NOT be valid/)
186
+ await assert.rejects(validate(manifest), (error) =>
187
+ /must NOT be valid/.test(error.message)
188
+ )
146
189
  })
147
190
  })
148
191
 
149
192
  describe('operations', () => {
150
- it('should be object', () => {
193
+ it('should be object', async () => {
151
194
  manifest.operations.get = 'bar'
152
- expect(() => validate(manifest)).toThrow(/must be object/)
195
+ await assert.rejects(validate(manifest), (error) =>
196
+ /must be object/.test(error.message)
197
+ )
153
198
  })
154
199
 
155
- it('should not have additional properties', () => {
200
+ it('should not have additional properties', async () => {
156
201
  manifest.operations.get.foo = 'bar'
157
- expect(() => validate(manifest)).toThrow(/additional property/)
202
+ await assert.rejects(validate(manifest))
158
203
  })
159
204
 
160
- it('should have type (transition or observation)', () => {
205
+ it('should have type (transition or observation)', async () => {
161
206
  delete manifest.operations.get.type
162
- expect(() => validate(manifest)).toThrow()
207
+ await assert.rejects(validate(manifest))
163
208
 
164
209
  manifest.operations.get.type = 'foo'
165
- expect(() => validate(manifest)).toThrow(/one of the allowed values/)
210
+ await assert.rejects(validate(manifest), (error) =>
211
+ /one of the allowed values/.test(error.message)
212
+ )
166
213
  })
167
214
 
168
- it('should forbid explicit loop', () => {
215
+ it('should forbid explicit loop', async () => {
169
216
  manifest.operations.get.bindings = ['@toa.io/bindings.loop']
170
- expect(() => validate(manifest)).toThrow(/must NOT be valid/)
217
+ await assert.rejects(validate(manifest), (error) =>
218
+ /must NOT be valid/.test(error.message)
219
+ )
171
220
  })
172
221
 
173
- it('should forbid query: false for observations', () => {
222
+ it('should forbid query: false for observations', async () => {
174
223
  manifest.operations.get.query = false
175
- expect(() => validate(manifest)).toThrow(/must NOT be valid/)
224
+ await assert.rejects(validate(manifest), (error) =>
225
+ /must NOT be valid/.test(error.message)
226
+ )
176
227
  })
177
228
 
178
- it.each([
179
- ['computation', 'compute'],
180
- ['effect', 'affect']
181
- ])('should set query: false for %s', async (_, operation) => {
182
- validate(manifest)
229
+ for (const [_, operation] of [['computation', 'compute']])
230
+ it(`should set query: false for ${_}`, async () => {
231
+ validate(manifest)
183
232
 
184
- expect(manifest.operations[operation].query).toBe(false)
185
- })
233
+ assert.strictEqual(manifest.operations[operation].query, false)
234
+ })
186
235
 
187
236
  describe('scope', () => {
188
- it('should have scope', () => {
237
+ it('should have scope', async () => {
189
238
  delete manifest.operations.get.scope
190
- expect(() => validate(manifest)).toThrow(/required property/)
239
+ await assert.rejects(validate(manifest), (error) =>
240
+ /required property/.test(error.message)
241
+ )
191
242
  })
192
243
 
193
- it('should allow only entity or set for observations', () => {
244
+ it('should allow only entity or set for observations', async () => {
194
245
  manifest.operations.get.scope = 'changeset'
195
- expect(() => validate(manifest)).toThrow(/allowed values/)
246
+ await assert.rejects(validate(manifest), (error) =>
247
+ /allowed values/.test(error.message)
248
+ )
196
249
  })
197
250
 
198
- it('should allow only entity for transitions', () => {
251
+ it('should allow only entity for transitions', async () => {
199
252
  manifest.operations.add.scope = 'changeset'
200
- expect(() => validate(manifest)).toThrow(/allowed values/)
253
+ await assert.rejects(validate(manifest), (error) =>
254
+ /allowed values/.test(error.message)
255
+ )
201
256
 
202
257
  manifest.operations.add.scope = 'set'
203
- expect(() => validate(manifest)).toThrow(/allowed values/)
258
+ await assert.rejects(validate(manifest), (error) =>
259
+ /allowed values/.test(error.message)
260
+ )
204
261
  })
205
262
 
206
- it('should allow only changeset for assignments', () => {
263
+ it('should refuse the names the scopes had before `entry` and `entries`', async () => {
264
+ for (const scope of ['object', 'objects']) {
265
+ manifest.operations.get.scope = scope
266
+ await assert.rejects(validate(manifest), (error) => /allowed values/.test(error.message))
267
+ }
268
+ })
269
+
270
+ it('should allow only changeset for assignments', async () => {
207
271
  manifest.operations.set.scope = 'changeset'
208
- expect(() => validate(manifest)).not.toThrow()
272
+ await assert.doesNotReject(validate(manifest))
209
273
 
210
274
  manifest.operations.set.scope = 'set'
211
- expect(() => validate(manifest)).toThrow(/allowed values/)
275
+ await assert.rejects(validate(manifest), (error) =>
276
+ /allowed values/.test(error.message)
277
+ )
278
+ })
279
+ })
280
+
281
+ describe('once', () => {
282
+ it('should be true, false or a number of seconds', async () => {
283
+ for (const once of [true, false, 600, 86400]) {
284
+ manifest.operations.add.once = once
285
+ await assert.doesNotReject(validate(manifest), `once: ${once}`)
286
+ }
287
+ })
288
+
289
+ it('should stay true rather than be coerced into a number', async () => {
290
+ manifest.operations.add.once = true
291
+ await validate(manifest)
292
+
293
+ assert.strictEqual(manifest.operations.add.once, true)
294
+ })
295
+
296
+ it('should not be a window shorter than ten minutes', async () => {
297
+ manifest.operations.add.once = 599
298
+ await assert.rejects(validate(manifest), (error) => /must be >= 600/.test(error.message))
299
+ })
300
+
301
+ it('should not be a fraction of a second', async () => {
302
+ manifest.operations.add.once = 600.5
303
+ await assert.rejects(validate(manifest))
304
+ })
305
+
306
+ it('should throw for an operation that takes none', async () => {
307
+ manifest.operations.get.once = 600
308
+ await assert.rejects(validate(manifest))
212
309
  })
213
310
  })
214
311
 
215
312
  describe('concurrency', () => {
216
- it('should be required for transitions', () => {
313
+ it('should be required for transitions', async () => {
217
314
  delete manifest.operations.add.concurrency
218
- expect(() => validate(manifest)).toThrow(/required property/)
315
+ await assert.rejects(validate(manifest), (error) =>
316
+ /required property/.test(error.message)
317
+ )
219
318
  })
220
319
 
221
- it('should throw for observations, assignments', () => {
320
+ it('should throw for observations, assignments', async () => {
222
321
  manifest.operations.get.concurrency = 'none'
223
- expect(() => validate(manifest)).toThrow()
322
+ await assert.rejects(validate(manifest))
224
323
  delete manifest.operations.get.concurrency
225
324
 
226
325
  manifest.operations.set.concurrency = 'none'
227
- expect(() => validate(manifest)).toThrow()
326
+ await assert.rejects(validate(manifest))
228
327
  })
229
328
  })
230
329
 
231
330
  describe('input, output', () => {
232
- it('should be schema', () => {
331
+ it('should be schema', async () => {
233
332
  manifest.operations.get.input = { properties: { foo: 'bar' } }
234
- expect(() => validate(manifest)).toThrow()
333
+ await assert.rejects(validate(manifest))
235
334
 
236
335
  delete manifest.operations.get.input
237
336
  manifest.operations.get.output = { properties: { foo: 'bar' } }
238
- expect(() => validate(manifest)).toThrow()
337
+ await assert.rejects(validate(manifest))
239
338
  })
240
339
  })
241
340
  })
242
341
 
243
342
  describe('receivers', () => {
244
- it('should throw if transition points to undefined operation', () => {
343
+ it('should throw if transition points to undefined operation', async () => {
245
344
  manifest.receivers['foo.bar.happened'].operation = 'notExists'
246
345
 
247
- expect(() => validate(manifest)).toThrow(/refers to undefined operation/)
346
+ await assert.rejects(validate(manifest), (error) =>
347
+ /refers to undefined operation/.test(error.message)
348
+ )
248
349
  })
249
350
 
250
- it('should throw if transition points to observation', () => {
351
+ it('should throw if transition points to observation', async () => {
251
352
  manifest.receivers['foo.bar.happened'].operation = 'get'
252
353
 
253
- expect(() => validate(manifest)).toThrow(/one of the allowed types/)
354
+ await assert.rejects(validate(manifest), (error) =>
355
+ /of the allowed types/.test(error.message)
356
+ )
254
357
  })
255
358
 
256
359
  it('should throw if source has a name `context`', async () => {
257
360
  manifest.receivers['foo.bar.happened'].source = 'context'
258
361
 
259
- expect(() => validate(manifest)).toThrow(/must NOT be valid/)
362
+ await assert.rejects(validate(manifest), (error) =>
363
+ /must NOT be valid/.test(error.message)
364
+ )
365
+ })
366
+ })
367
+
368
+ describe('stateful', () => {
369
+ it('should be allowed on an operation', async () => {
370
+ const [operation] = Object.values(manifest.operations)
371
+
372
+ operation.stateful = true
373
+
374
+ await assert.doesNotReject(validate(manifest))
375
+ })
376
+
377
+ it('should be a boolean', async () => {
378
+ const [operation] = Object.values(manifest.operations)
379
+
380
+ operation.stateful = 'yes'
381
+
382
+ await assert.rejects(validate(manifest), (error) => /must be boolean/.test(error.message))
383
+ })
384
+ })
385
+
386
+ describe('stream', () => {
387
+ beforeEach(() => {
388
+ manifest.operations.read = {
389
+ type: 'computation',
390
+ scope: 'none',
391
+ query: false,
392
+ bridge: 'whatever',
393
+ bindings: ['@toa.io/bindings.amqp'],
394
+ stream: 'content'
395
+ }
396
+ })
397
+
398
+ it('should reject an operation whose bindings carry no stream', async () => {
399
+ await assert.rejects(validate(manifest), (error) =>
400
+ /takes a stream, which none of its bindings carries/.test(error.message)
401
+ )
402
+ })
403
+
404
+ it('should accept an operation served where it is composed', async () => {
405
+ manifest.operations.read.bindings = []
406
+
407
+ await assert.doesNotReject(validate(manifest))
408
+ })
409
+
410
+ it('should accept an operation bound to a binding that carries a stream', async () => {
411
+ manifest.operations.read.bindings = ['@toa.io/bindings.http']
412
+
413
+ await assert.doesNotReject(validate(manifest))
414
+ })
415
+
416
+ it('should reject a transition that takes a stream', async () => {
417
+ manifest.operations.add.stream = 'content'
418
+
419
+ await assert.rejects(validate(manifest), (error) =>
420
+ /must be equal to one of the allowed values/.test(error.message)
421
+ )
260
422
  })
261
423
  })
@@ -1,5 +1,3 @@
1
- 'use strict'
2
-
3
1
  const component = (id) => {
4
2
  const [namespace, name] = id.split('.')
5
3
 
@@ -16,12 +14,11 @@ const component = (id) => {
16
14
  }
17
15
  }
18
16
 
19
- const context = {
17
+ export const context = {
20
18
  runtime: '0.0.0',
21
19
  name: 'test',
22
20
  description: 'context fixture',
23
21
  version: '0.0.0',
24
- packages: 'namespaces/**/*',
25
22
  registry: 'localhost:5000',
26
23
  components: [
27
24
  component('a.b'),
@@ -43,7 +40,7 @@ const context = {
43
40
  }
44
41
 
45
42
  /** @type {Array<toa.norm.context.Composition>} */
46
- const compositions = [
43
+ export const compositions = [
47
44
  ...context.compositions,
48
45
  {
49
46
  name: 'd-a',
@@ -54,6 +51,3 @@ const compositions = [
54
51
  components: [component('d.b')]
55
52
  }
56
53
  ]
57
-
58
- exports.context = context
59
- exports.compositions = compositions
@@ -1,9 +1,11 @@
1
- 'use strict'
1
+ import { it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { isDeepStrictEqual } from 'node:util'
2
4
 
3
- const clone = require('clone-deep')
5
+ import clone from 'clone-deep'
4
6
 
5
- const { complete } = require('../../src/.context')
6
- const fixtures = require('./complete.fixtures')
7
+ import { complete } from '../../src/.context/index.js'
8
+ import * as fixtures from './complete.fixtures.js'
7
9
 
8
10
  /** @type {toa.norm.Context} */
9
11
  let context
@@ -14,8 +16,12 @@ beforeEach(() => {
14
16
  })
15
17
 
16
18
  it('should complete compositions', () => {
17
- expect(context.compositions.length).toEqual(fixtures.compositions.length)
18
- expect(context.compositions).toEqual(expect.arrayContaining(fixtures.compositions))
19
+ assert.deepStrictEqual(context.compositions.length, fixtures.compositions.length)
20
+ assert.ok(
21
+ fixtures.compositions.every((item) =>
22
+ context.compositions.some((candidate) => isDeepStrictEqual(candidate, item))
23
+ )
24
+ )
19
25
  })
20
26
 
21
27
  it('should create if compositions are not set', () => {
@@ -26,6 +32,6 @@ it('should create if compositions are not set', () => {
26
32
  components: [component]
27
33
  }))
28
34
 
29
- expect(() => complete(context)).not.toThrow()
30
- expect(context.compositions).toStrictEqual(compositions)
35
+ assert.doesNotThrow(() => complete(context))
36
+ assert.deepStrictEqual(context.compositions, compositions)
31
37
  })
@@ -0,0 +1,57 @@
1
+ import { it, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { converge } from '../../src/.context/index.js'
5
+
6
+ const REFERENCE = '@toa.io/extensions.convergence'
7
+
8
+ /** @type {object} */
9
+ let context
10
+
11
+ /** @type {object[]} */
12
+ let components
13
+
14
+ const component = (id, properties = {}) => ({ id, entity: {}, ...properties })
15
+
16
+ beforeEach(() => {
17
+ context = { annotations: { [REFERENCE]: { priority: 0 } } }
18
+
19
+ components = [
20
+ component('store.orders'),
21
+ // what an extension ships reaches a deployment the same way and converges the same way:
22
+ // an application whose identity does not converge has no users in its second region
23
+ component('identity.basic'),
24
+ component('exposition.octets', { entity: undefined })
25
+ ]
26
+ })
27
+
28
+ it('should give the extension to every component that stores anything', () => {
29
+ converge(context, components)
30
+
31
+ assert.deepEqual(components[0].extensions, { [REFERENCE]: null })
32
+ assert.deepEqual(components[1].extensions, { [REFERENCE]: null })
33
+ })
34
+
35
+ it('should leave a component that stores nothing alone', () => {
36
+ converge(context, components)
37
+
38
+ assert.equal(components[2].extensions, undefined)
39
+ })
40
+
41
+ it('should do nothing where the context is not a region', () => {
42
+ delete context.annotations
43
+
44
+ converge(context, components)
45
+
46
+ for (const one of components) assert.equal(one.extensions, undefined)
47
+ })
48
+
49
+ it('should leave what a manifest declared as it was', () => {
50
+ const declaration = { some: 'declaration' }
51
+
52
+ components[0].extensions = { [REFERENCE]: declaration }
53
+
54
+ converge(context, components)
55
+
56
+ assert.equal(components[0].extensions[REFERENCE], declaration)
57
+ })