@codifycli/plugin-core 1.0.0-beta1

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 (152) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +30 -0
  3. package/.github/workflows/release.yaml +19 -0
  4. package/.github/workflows/unit-test-ci.yaml +18 -0
  5. package/.prettierrc.json +1 -0
  6. package/bin/build.js +189 -0
  7. package/dist/bin/build.d.ts +1 -0
  8. package/dist/bin/build.js +80 -0
  9. package/dist/bin/deploy-plugin.d.ts +2 -0
  10. package/dist/bin/deploy-plugin.js +8 -0
  11. package/dist/common/errors.d.ts +8 -0
  12. package/dist/common/errors.js +24 -0
  13. package/dist/entities/change-set.d.ts +24 -0
  14. package/dist/entities/change-set.js +152 -0
  15. package/dist/entities/errors.d.ts +4 -0
  16. package/dist/entities/errors.js +7 -0
  17. package/dist/entities/plan-types.d.ts +25 -0
  18. package/dist/entities/plan-types.js +1 -0
  19. package/dist/entities/plan.d.ts +15 -0
  20. package/dist/entities/plan.js +127 -0
  21. package/dist/entities/plugin.d.ts +16 -0
  22. package/dist/entities/plugin.js +80 -0
  23. package/dist/entities/resource-options.d.ts +31 -0
  24. package/dist/entities/resource-options.js +76 -0
  25. package/dist/entities/resource-types.d.ts +11 -0
  26. package/dist/entities/resource-types.js +1 -0
  27. package/dist/entities/resource.d.ts +42 -0
  28. package/dist/entities/resource.js +303 -0
  29. package/dist/entities/stateful-parameter.d.ts +29 -0
  30. package/dist/entities/stateful-parameter.js +46 -0
  31. package/dist/entities/transform-parameter.d.ts +4 -0
  32. package/dist/entities/transform-parameter.js +2 -0
  33. package/dist/errors.d.ts +4 -0
  34. package/dist/errors.js +7 -0
  35. package/dist/index.d.ts +20 -0
  36. package/dist/index.js +26 -0
  37. package/dist/messages/handlers.d.ts +14 -0
  38. package/dist/messages/handlers.js +134 -0
  39. package/dist/messages/sender.d.ts +11 -0
  40. package/dist/messages/sender.js +57 -0
  41. package/dist/plan/change-set.d.ts +53 -0
  42. package/dist/plan/change-set.js +153 -0
  43. package/dist/plan/plan-types.d.ts +23 -0
  44. package/dist/plan/plan-types.js +1 -0
  45. package/dist/plan/plan.d.ts +66 -0
  46. package/dist/plan/plan.js +328 -0
  47. package/dist/plugin/plugin.d.ts +24 -0
  48. package/dist/plugin/plugin.js +200 -0
  49. package/dist/pty/background-pty.d.ts +21 -0
  50. package/dist/pty/background-pty.js +127 -0
  51. package/dist/pty/index.d.ts +50 -0
  52. package/dist/pty/index.js +20 -0
  53. package/dist/pty/promise-queue.d.ts +5 -0
  54. package/dist/pty/promise-queue.js +26 -0
  55. package/dist/pty/seqeuntial-pty.d.ts +17 -0
  56. package/dist/pty/seqeuntial-pty.js +119 -0
  57. package/dist/pty/vitest.config.d.ts +2 -0
  58. package/dist/pty/vitest.config.js +11 -0
  59. package/dist/resource/config-parser.d.ts +11 -0
  60. package/dist/resource/config-parser.js +21 -0
  61. package/dist/resource/parsed-resource-settings.d.ts +47 -0
  62. package/dist/resource/parsed-resource-settings.js +196 -0
  63. package/dist/resource/resource-controller.d.ts +36 -0
  64. package/dist/resource/resource-controller.js +402 -0
  65. package/dist/resource/resource-settings.d.ts +303 -0
  66. package/dist/resource/resource-settings.js +147 -0
  67. package/dist/resource/resource.d.ts +144 -0
  68. package/dist/resource/resource.js +44 -0
  69. package/dist/resource/stateful-parameter.d.ts +165 -0
  70. package/dist/resource/stateful-parameter.js +94 -0
  71. package/dist/scripts/deploy.d.ts +1 -0
  72. package/dist/scripts/deploy.js +2 -0
  73. package/dist/stateful-parameter/stateful-parameter-controller.d.ts +21 -0
  74. package/dist/stateful-parameter/stateful-parameter-controller.js +81 -0
  75. package/dist/stateful-parameter/stateful-parameter.d.ts +144 -0
  76. package/dist/stateful-parameter/stateful-parameter.js +43 -0
  77. package/dist/test.d.ts +1 -0
  78. package/dist/test.js +5 -0
  79. package/dist/utils/codify-spawn.d.ts +29 -0
  80. package/dist/utils/codify-spawn.js +136 -0
  81. package/dist/utils/debug.d.ts +2 -0
  82. package/dist/utils/debug.js +10 -0
  83. package/dist/utils/file-utils.d.ts +23 -0
  84. package/dist/utils/file-utils.js +186 -0
  85. package/dist/utils/functions.d.ts +12 -0
  86. package/dist/utils/functions.js +74 -0
  87. package/dist/utils/index.d.ts +46 -0
  88. package/dist/utils/index.js +271 -0
  89. package/dist/utils/internal-utils.d.ts +12 -0
  90. package/dist/utils/internal-utils.js +74 -0
  91. package/dist/utils/load-resources.d.ts +1 -0
  92. package/dist/utils/load-resources.js +46 -0
  93. package/dist/utils/package-json-utils.d.ts +12 -0
  94. package/dist/utils/package-json-utils.js +34 -0
  95. package/dist/utils/pty-local-storage.d.ts +2 -0
  96. package/dist/utils/pty-local-storage.js +2 -0
  97. package/dist/utils/spawn-2.d.ts +5 -0
  98. package/dist/utils/spawn-2.js +7 -0
  99. package/dist/utils/spawn.d.ts +29 -0
  100. package/dist/utils/spawn.js +124 -0
  101. package/dist/utils/utils.d.ts +18 -0
  102. package/dist/utils/utils.js +86 -0
  103. package/dist/utils/verbosity-level.d.ts +5 -0
  104. package/dist/utils/verbosity-level.js +9 -0
  105. package/package.json +59 -0
  106. package/rollup.config.js +24 -0
  107. package/src/common/errors.test.ts +43 -0
  108. package/src/common/errors.ts +31 -0
  109. package/src/errors.ts +8 -0
  110. package/src/index.test.ts +6 -0
  111. package/src/index.ts +30 -0
  112. package/src/messages/handlers.test.ts +329 -0
  113. package/src/messages/handlers.ts +181 -0
  114. package/src/messages/sender.ts +69 -0
  115. package/src/plan/change-set.test.ts +280 -0
  116. package/src/plan/change-set.ts +236 -0
  117. package/src/plan/plan-types.ts +27 -0
  118. package/src/plan/plan.test.ts +413 -0
  119. package/src/plan/plan.ts +499 -0
  120. package/src/plugin/plugin.test.ts +533 -0
  121. package/src/plugin/plugin.ts +291 -0
  122. package/src/pty/background-pty.test.ts +69 -0
  123. package/src/pty/background-pty.ts +154 -0
  124. package/src/pty/index.test.ts +129 -0
  125. package/src/pty/index.ts +66 -0
  126. package/src/pty/promise-queue.ts +33 -0
  127. package/src/pty/seqeuntial-pty.ts +151 -0
  128. package/src/pty/sequential-pty.test.ts +194 -0
  129. package/src/resource/config-parser.ts +42 -0
  130. package/src/resource/parsed-resource-settings.test.ts +186 -0
  131. package/src/resource/parsed-resource-settings.ts +307 -0
  132. package/src/resource/resource-controller-stateful-mode.test.ts +253 -0
  133. package/src/resource/resource-controller.test.ts +1081 -0
  134. package/src/resource/resource-controller.ts +563 -0
  135. package/src/resource/resource-settings.test.ts +1213 -0
  136. package/src/resource/resource-settings.ts +545 -0
  137. package/src/resource/resource.ts +157 -0
  138. package/src/stateful-parameter/stateful-parameter-controller.test.ts +244 -0
  139. package/src/stateful-parameter/stateful-parameter-controller.ts +111 -0
  140. package/src/stateful-parameter/stateful-parameter.ts +160 -0
  141. package/src/utils/debug.ts +11 -0
  142. package/src/utils/file-utils.test.ts +7 -0
  143. package/src/utils/file-utils.ts +231 -0
  144. package/src/utils/functions.ts +103 -0
  145. package/src/utils/index.ts +340 -0
  146. package/src/utils/internal-utils.test.ts +52 -0
  147. package/src/utils/pty-local-storage.ts +3 -0
  148. package/src/utils/test-utils.test.ts +96 -0
  149. package/src/utils/verbosity-level.ts +11 -0
  150. package/tsconfig.json +26 -0
  151. package/tsconfig.test.json +9 -0
  152. package/vitest.config.ts +10 -0
@@ -0,0 +1,1213 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Plan } from '../plan/plan.js';
3
+ import { spy } from 'sinon';
4
+ import { OS, ParameterOperation, ResourceOperation } from 'codify-schemas';
5
+ import {
6
+ TestArrayStatefulParameter,
7
+ TestConfig,
8
+ testPlan,
9
+ TestResource,
10
+ TestStatefulParameter
11
+ } from '../utils/test-utils.test.js';
12
+ import { ArrayParameterSetting, ParameterSetting, ResourceSettings } from './resource-settings.js';
13
+ import { ResourceController } from './resource-controller.js';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { z } from 'zod';
17
+
18
+ describe('Resource parameter tests', () => {
19
+ it('Generates a resource plan that includes stateful parameters (create)', async () => {
20
+ const statefulParameter = spy(new class extends TestStatefulParameter {
21
+ async refresh(): Promise<string | null> {
22
+ return null;
23
+ }
24
+ })
25
+
26
+ const resource = new class extends TestResource {
27
+ getSettings(): ResourceSettings<TestConfig> {
28
+ return {
29
+ id: 'type',
30
+ operatingSystems: [OS.Darwin],
31
+ parameterSettings: {
32
+ propA: { type: 'stateful', definition: statefulParameter }
33
+ },
34
+ };
35
+ }
36
+
37
+ async refresh(): Promise<any> {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ const controller = new ResourceController(resource);
43
+ const plan = await controller.plan(
44
+ { type: 'type' },
45
+ {
46
+ propA: 'a',
47
+ propB: 10
48
+ },
49
+ null,
50
+ false
51
+ )
52
+
53
+ expect(statefulParameter.refresh.notCalled).to.be.true;
54
+ expect(plan.currentConfig).to.be.null;
55
+ expect(plan.desiredConfig).toMatchObject({
56
+ propA: 'a',
57
+ propB: 10
58
+ })
59
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
60
+ })
61
+
62
+ it('supports the creation of stateful parameters', async () => {
63
+
64
+ const statefulParameter = new class extends TestStatefulParameter {
65
+ async refresh(): Promise<string | null> {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ const statefulParameterSpy = spy(statefulParameter);
71
+
72
+ const resource = new class extends TestResource {
73
+ getSettings(): ResourceSettings<TestConfig> {
74
+ return {
75
+ id: 'type',
76
+ operatingSystems: [OS.Darwin],
77
+ parameterSettings: {
78
+ propA: { type: 'stateful', definition: statefulParameterSpy }
79
+ },
80
+ }
81
+ }
82
+ }
83
+
84
+ const controller = new ResourceController(resource);
85
+ const resourceSpy = spy(resource);
86
+
87
+ await controller.apply(
88
+ testPlan<TestConfig>({
89
+ desired: { propA: 'a', propB: 0, propC: 'c' }
90
+ })
91
+ );
92
+
93
+ expect(statefulParameterSpy.add.calledOnce).to.be.true;
94
+ expect(resourceSpy.create.calledOnce).to.be.true;
95
+ })
96
+
97
+ it('supports the modification of stateful parameters', async () => {
98
+ const statefulParameter = new class extends TestStatefulParameter {
99
+ async refresh(): Promise<string | null> {
100
+ return 'b';
101
+ }
102
+ }
103
+
104
+ const statefulParameterSpy = spy(statefulParameter);
105
+
106
+ const resource = new class extends TestResource {
107
+
108
+ getSettings(): ResourceSettings<TestConfig> {
109
+ return {
110
+ id: 'type',
111
+ operatingSystems: [OS.Darwin],
112
+ parameterSettings: {
113
+ propA: { type: 'stateful', definition: statefulParameterSpy },
114
+ propB: { canModify: true },
115
+ }
116
+ };
117
+ }
118
+
119
+ async refresh(): Promise<Partial<TestConfig> | null> {
120
+ return { propB: -1, propC: 'b' }
121
+ }
122
+ }
123
+
124
+ const controller = new ResourceController(resource);
125
+
126
+ const plan = await controller.plan(
127
+ { type: 'type' },
128
+ { propA: 'a', propB: 0, propC: 'b' },
129
+ null,
130
+ false
131
+ )
132
+
133
+ const resourceSpy = spy(resource);
134
+ await controller.apply(plan);
135
+
136
+ expect(statefulParameterSpy.modify.calledOnce).to.be.true;
137
+ expect(resourceSpy.modify.calledOnce).to.be.true;
138
+ })
139
+
140
+ it('Allows stateful parameters to have default values', async () => {
141
+ const statefulParameter = spy(new class extends TestStatefulParameter {
142
+ getSettings(): ParameterSetting {
143
+ return {
144
+ default: 'abc',
145
+ operatingSystems: [OS.Darwin],
146
+ };
147
+ }
148
+
149
+ async refresh(): Promise<string | null> {
150
+ return null;
151
+ }
152
+ });
153
+
154
+ const resource = new class extends TestResource {
155
+ getSettings(): ResourceSettings<TestConfig> {
156
+ return {
157
+ id: 'type',
158
+ operatingSystems: [OS.Darwin],
159
+ parameterSettings: {
160
+ propA: { type: 'stateful', definition: statefulParameter }
161
+ },
162
+ }
163
+ }
164
+
165
+ async refresh(): Promise<any> {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ const controller = new ResourceController(resource);
171
+ const plan = await controller.plan({
172
+ type: 'type',
173
+ }, {}, null, false)
174
+
175
+ expect(statefulParameter.refresh.notCalled).to.be.true;
176
+ expect(plan.currentConfig).to.be.null;
177
+ expect(plan.desiredConfig).toMatchObject({
178
+ propA: 'abc',
179
+ })
180
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.CREATE);
181
+ })
182
+
183
+ it('Filters array results in stateless mode to prevent modify from being called', async () => {
184
+ const statefulParameter = new class extends TestStatefulParameter {
185
+ getSettings(): ParameterSetting {
186
+ return { type: 'array' }
187
+ }
188
+
189
+ async refresh(): Promise<any | null> {
190
+ return ['a', 'b', 'c', 'd']
191
+ }
192
+ }
193
+
194
+ const statefulParameterSpy = spy(statefulParameter);
195
+
196
+ const resource = new class extends TestResource {
197
+ getSettings(): ResourceSettings<TestConfig> {
198
+ return {
199
+ id: 'type',
200
+ operatingSystems: [OS.Darwin],
201
+ parameterSettings: {
202
+ propA: { type: 'stateful', definition: statefulParameterSpy },
203
+ },
204
+ }
205
+ }
206
+
207
+ async refresh(): Promise<Partial<TestConfig> | null> {
208
+ return {};
209
+ }
210
+ }
211
+
212
+ const controller = new ResourceController(resource);
213
+ const plan = await controller.plan({ type: 'type' }, { propA: ['a', 'b'] } as any, null, false)
214
+
215
+ expect(plan).toMatchObject({
216
+ changeSet: {
217
+ operation: ResourceOperation.NOOP,
218
+ }
219
+ })
220
+ })
221
+
222
+ it('Filters array results in stateless mode to prevent modify from being called 2', async () => {
223
+ const statefulParameter = new class extends TestStatefulParameter {
224
+ async refresh(): Promise<any | null> {
225
+ return ['a', 'b']
226
+ }
227
+ }
228
+
229
+ const statefulParameterSpy = spy(statefulParameter);
230
+
231
+ const resource = new class extends TestResource {
232
+ getSettings(): ResourceSettings<TestConfig> {
233
+ return {
234
+ id: 'type',
235
+ operatingSystems: [OS.Darwin],
236
+ parameterSettings: {
237
+ propA: { type: 'stateful', definition: statefulParameterSpy }
238
+ },
239
+ }
240
+ }
241
+
242
+ async refresh(): Promise<Partial<TestConfig> | null> {
243
+ return {};
244
+ }
245
+ }
246
+
247
+ const controller = new ResourceController(resource);
248
+ const plan = await controller.plan({ type: 'type' }, { propA: ['a', 'b', 'c', 'd'] } as any, null, false)
249
+
250
+ expect(plan).toMatchObject({
251
+ changeSet: {
252
+ operation: ResourceOperation.MODIFY,
253
+ }
254
+ })
255
+ })
256
+
257
+ it('Can accept a custom filter function to filter in stateless mode', async () => {
258
+ const resource = new class extends TestResource {
259
+ getSettings(): ResourceSettings<TestConfig> {
260
+ return {
261
+ id: 'type',
262
+ operatingSystems: [OS.Darwin],
263
+ parameterSettings: {
264
+ hosts: {
265
+ type: 'array',
266
+ isElementEqual: 'object',
267
+ filterInStatelessMode: (desired, current) => {
268
+ return current.filter((d) => desired.some((c) => d.Host === c.Host))
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
276
+ return {
277
+ hosts: [
278
+ {
279
+ Host: '*',
280
+ AddKeysToAgent: 'yes',
281
+ IdentityFile: 'id_ed25519'
282
+ },
283
+ {
284
+ Host: 'github.com',
285
+ AddKeysToAgent: 'yes',
286
+ UseKeychain: 'yes',
287
+ IgnoreUnknown: 'UseKeychain',
288
+ IdentityFile: '~/.ssh/id_ed25519',
289
+ }
290
+ ]
291
+ }
292
+ }
293
+ }
294
+
295
+ const controller = new ResourceController(resource);
296
+ const plan = await controller.plan(
297
+ { type: 'type' },
298
+ {
299
+ hosts: [
300
+ {
301
+ Host: 'new.com',
302
+ AddKeysToAgent: 'yes',
303
+ IdentityFile: '~/.ssh/id_ed25519'
304
+ },
305
+ {
306
+ Host: 'github.com',
307
+ AddKeysToAgent: 'yes',
308
+ UseKeychain: 'yes',
309
+ }
310
+ ]
311
+ },
312
+ null,
313
+ false
314
+ );
315
+
316
+ expect(plan).toMatchObject({
317
+ 'changeSet': {
318
+ 'operation': 'recreate',
319
+ 'parameterChanges': [
320
+ {
321
+ 'name': 'hosts',
322
+ 'previousValue': [
323
+ {
324
+ 'Host': 'github.com',
325
+ 'AddKeysToAgent': 'yes',
326
+ 'UseKeychain': 'yes',
327
+ 'IgnoreUnknown': 'UseKeychain',
328
+ 'IdentityFile': '~/.ssh/id_ed25519'
329
+ }
330
+ ],
331
+ 'newValue': [
332
+ {
333
+ 'Host': 'new.com',
334
+ 'AddKeysToAgent': 'yes',
335
+ 'IdentityFile': '~/.ssh/id_ed25519'
336
+ },
337
+ {
338
+ 'Host': 'github.com',
339
+ 'AddKeysToAgent': 'yes',
340
+ 'UseKeychain': 'yes'
341
+ }
342
+ ],
343
+ 'operation': 'modify'
344
+ }
345
+ ]
346
+ },
347
+ })
348
+ })
349
+
350
+ it('Uses isElementEqual for stateless mode filtering if available', async () => {
351
+ const statefulParameter = new class extends TestArrayStatefulParameter {
352
+ getSettings(): ArrayParameterSetting {
353
+ return {
354
+ type: 'array',
355
+ isElementEqual: (desired, current) => {
356
+ return current.includes(desired)
357
+ },
358
+ }
359
+ }
360
+
361
+ async refresh(): Promise<any | null> {
362
+ return ['3.11.9']
363
+ }
364
+ }
365
+
366
+ const statefulParameterSpy = spy(statefulParameter);
367
+
368
+ const resource = new class extends TestResource {
369
+ getSettings(): ResourceSettings<TestConfig> {
370
+ return {
371
+ id: 'type',
372
+ operatingSystems: [OS.Darwin],
373
+ parameterSettings: {
374
+ propA: { type: 'stateful', definition: statefulParameterSpy }
375
+ },
376
+ }
377
+ }
378
+
379
+ async refresh(): Promise<Partial<TestConfig> | null> {
380
+ return {};
381
+ }
382
+ }
383
+
384
+ const controller = new ResourceController(resource);
385
+ const plan = await controller.plan({ type: 'type' }, { propA: ['3.11'] } as any, null, false)
386
+
387
+ expect(plan).toMatchObject({
388
+ changeSet: {
389
+ operation: ResourceOperation.NOOP,
390
+ }
391
+ })
392
+ })
393
+
394
+ it('Plans stateful parameters in the order specified', async () => {
395
+ const statefulParameterA = spy(new class extends TestStatefulParameter {
396
+ async refresh(): Promise<any | null> {
397
+ return performance.now()
398
+ }
399
+ });
400
+
401
+ const statefulParameterB = spy(new class extends TestStatefulParameter {
402
+ async refresh(): Promise<any | null> {
403
+ return performance.now()
404
+ }
405
+ });
406
+
407
+ const statefulParameterC = spy(new class extends TestStatefulParameter {
408
+ async refresh(): Promise<any | null> {
409
+ return performance.now()
410
+ }
411
+ });
412
+
413
+ const statefulParameterD = spy(new class extends TestStatefulParameter {
414
+ async refresh(): Promise<any | null> {
415
+ return performance.now()
416
+ }
417
+ });
418
+
419
+ const statefulParameterE = spy(new class extends TestStatefulParameter {
420
+ async refresh(): Promise<any | null> {
421
+ return performance.now()
422
+ }
423
+ });
424
+
425
+ const resource = spy(new class extends TestResource {
426
+ getSettings(): ResourceSettings<TestConfig> {
427
+ return {
428
+ id: 'resourceType',
429
+ operatingSystems: [OS.Darwin],
430
+ parameterSettings: {
431
+ propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
432
+ propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
433
+ propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
434
+ propD: { type: 'stateful', definition: statefulParameterD },
435
+ propE: { type: 'stateful', definition: statefulParameterE }
436
+ },
437
+ }
438
+ }
439
+
440
+ async refresh(): Promise<Partial<TestConfig> | null> {
441
+ return {};
442
+ }
443
+ });
444
+
445
+ const controller = new ResourceController(resource)
446
+ const plan = await controller.plan(
447
+ { type: 'resourceType' },
448
+ {
449
+ propA: 'propA',
450
+ propB: 10,
451
+ propC: 'propC',
452
+ propD: 'propD',
453
+ propE: 'propE',
454
+ }, null,
455
+ false
456
+ );
457
+
458
+ expect(plan.currentConfig?.propB).to.be.lessThan(plan.currentConfig?.propC as any);
459
+ expect(plan.currentConfig?.propC).to.be.lessThan(plan.currentConfig?.propA as any);
460
+ expect(plan.currentConfig?.propA).to.be.lessThan(plan.currentConfig?.propD as any);
461
+ expect(plan.currentConfig?.propD).to.be.lessThan(plan.currentConfig?.propE as any);
462
+ })
463
+
464
+ it('Applies stateful parameters in the order specified', async () => {
465
+ let timestampA;
466
+ const statefulParameterA = spy(new class extends TestStatefulParameter {
467
+ add = async (): Promise<void> => {
468
+ timestampA = performance.now();
469
+ }
470
+ modify = async (): Promise<void> => {
471
+ timestampA = performance.now();
472
+ }
473
+ remove = async (): Promise<void> => {
474
+ timestampA = performance.now();
475
+ }
476
+ });
477
+
478
+ let timestampB
479
+ const statefulParameterB = spy(new class extends TestStatefulParameter {
480
+ add = async (): Promise<void> => {
481
+ timestampB = performance.now();
482
+ }
483
+ modify = async (): Promise<void> => {
484
+ timestampB = performance.now();
485
+ }
486
+ remove = async (): Promise<void> => {
487
+ timestampB = performance.now();
488
+ }
489
+ });
490
+
491
+ let timestampC
492
+ const statefulParameterC = spy(new class extends TestStatefulParameter {
493
+ add = async (): Promise<void> => {
494
+ timestampC = performance.now();
495
+ }
496
+ modify = async (): Promise<void> => {
497
+ timestampC = performance.now();
498
+ }
499
+ remove = async (): Promise<void> => {
500
+ timestampC = performance.now();
501
+ }
502
+ });
503
+
504
+ const resource = spy(new class extends TestResource {
505
+ getSettings(): ResourceSettings<TestConfig> {
506
+ return {
507
+ id: 'resourceType',
508
+ operatingSystems: [OS.Darwin],
509
+ parameterSettings: {
510
+ propA: { type: 'stateful', definition: statefulParameterA, order: 3 },
511
+ propB: { type: 'stateful', definition: statefulParameterB, order: 1 },
512
+ propC: { type: 'stateful', definition: statefulParameterC, order: 2 },
513
+ },
514
+ removeStatefulParametersBeforeDestroy: true,
515
+ }
516
+ }
517
+ });
518
+
519
+ const controller = new ResourceController(resource);
520
+ await controller.apply(
521
+ Plan.fromResponse({
522
+ resourceType: 'resourceType',
523
+ operation: ResourceOperation.CREATE,
524
+ parameters: [
525
+ { name: 'propA', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
526
+ { name: 'propB', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
527
+ { name: 'propC', operation: ParameterOperation.ADD, previousValue: null, newValue: null },
528
+ ],
529
+ isStateful: false,
530
+ }, {}) as any
531
+ );
532
+
533
+ if (!timestampB || !timestampC || !timestampA) {
534
+ throw new Error('Variable not initialized')
535
+ }
536
+
537
+ expect(timestampB).to.be.lessThan(timestampC as any);
538
+ expect(timestampC).to.be.lessThan(timestampA as any);
539
+ timestampA = 0;
540
+ timestampB = 0;
541
+ timestampC = 0;
542
+
543
+ await controller.apply(
544
+ Plan.fromResponse({
545
+ resourceType: 'resourceType',
546
+ operation: ResourceOperation.MODIFY,
547
+ parameters: [
548
+ { name: 'propA', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
549
+ { name: 'propB', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
550
+ { name: 'propC', operation: ParameterOperation.MODIFY, previousValue: null, newValue: null },
551
+ ],
552
+ isStateful: false,
553
+ }, {}) as any
554
+ );
555
+
556
+ expect(timestampB).to.be.lessThan(timestampC as any);
557
+ expect(timestampC).to.be.lessThan(timestampA as any);
558
+ timestampA = 0;
559
+ timestampB = 0;
560
+ timestampC = 0;
561
+
562
+ await controller.apply(
563
+ Plan.fromResponse({
564
+ resourceType: 'resourceType',
565
+ operation: ResourceOperation.DESTROY,
566
+ parameters: [
567
+ { name: 'propA', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
568
+ { name: 'propB', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
569
+ { name: 'propC', operation: ParameterOperation.REMOVE, previousValue: null, newValue: null },
570
+ ],
571
+ isStateful: false,
572
+ }, {}) as any
573
+ );
574
+
575
+ expect(timestampB).to.be.lessThan(timestampC as any);
576
+ expect(timestampC).to.be.lessThan(timestampA as any);
577
+ })
578
+
579
+ it('Supports transform parameters', async () => {
580
+ const resource = spy(new class extends TestResource {
581
+ getSettings(): ResourceSettings<TestConfig> {
582
+ return {
583
+ id: 'resourceType',
584
+ operatingSystems: [OS.Darwin],
585
+ transformation: {
586
+ to: (desired) => ({
587
+ propA: 'propA',
588
+ propB: 10,
589
+ }),
590
+ from: (current) => ({
591
+ propA: 'propA',
592
+ propB: 10,
593
+ })
594
+ }
595
+ }
596
+ }
597
+
598
+ async refresh(): Promise<Partial<TestConfig> | null> {
599
+ return {
600
+ propA: 'propA',
601
+ propB: 10,
602
+ }
603
+ }
604
+ });
605
+
606
+ const controller = new ResourceController(resource);
607
+ const plan = await controller.plan({ type: 'resourceType' }, { propC: 'abc' } as any, null, false);
608
+
609
+ expect(resource.refresh.called).to.be.true;
610
+ expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
611
+ expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
612
+ expect(resource.refresh.getCall(0).firstArg['propC']).to.not.exist;
613
+
614
+ expect(plan.desiredConfig?.propA).to.eq('propA');
615
+ expect(plan.desiredConfig?.propB).to.eq(10);
616
+ expect(plan.desiredConfig?.propC).to.be.undefined;
617
+
618
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
619
+ })
620
+
621
+ it('Supports transform parameters for state parameters', async () => {
622
+ const resource = spy(new class extends TestResource {
623
+ getSettings(): ResourceSettings<TestConfig> {
624
+ return {
625
+ id: 'resourceType',
626
+ operatingSystems: [OS.Darwin],
627
+ transformation: {
628
+ to: (desired) => ({
629
+ propA: 'propA',
630
+ propB: 10,
631
+ }),
632
+ from: (desired) => ({
633
+ propA: 'propA',
634
+ propB: 10,
635
+ })
636
+ }
637
+ }
638
+ }
639
+
640
+ async refresh(): Promise<Partial<TestConfig> | null> {
641
+ return {
642
+ propA: 'propA',
643
+ propB: 10,
644
+ }
645
+ }
646
+ });
647
+
648
+ const controller = new ResourceController(resource);
649
+ const plan = await controller.plan({ type: 'resourceType' }, null, { propC: 'abc' }, true);
650
+
651
+ expect(resource.refresh.called).to.be.true;
652
+ expect(resource.refresh.getCall(0).firstArg['propA']).to.exist;
653
+ expect(resource.refresh.getCall(0).firstArg['propB']).to.exist;
654
+ expect(resource.refresh.getCall(0).firstArg['propC']).to.not.exist;
655
+
656
+ expect(plan.currentConfig?.propA).to.eq('propA');
657
+ expect(plan.currentConfig?.propB).to.eq(10);
658
+ expect(plan.currentConfig?.propC).to.be.undefined;
659
+ })
660
+
661
+ it('Allows import required parameters customization', () => {
662
+ const resource = new class extends TestResource {
663
+ getSettings(): ResourceSettings<TestConfig> {
664
+ return {
665
+ id: 'resourceType',
666
+ operatingSystems: [OS.Darwin],
667
+ importAndDestroy: {
668
+ requiredParameters: [
669
+ 'propA',
670
+ 'propB',
671
+ ]
672
+ }
673
+ }
674
+ }
675
+ };
676
+ })
677
+
678
+ it('Applies default input transformations', async () => {
679
+ const home = os.homedir()
680
+ const testPath = path.join(home, 'test/folder');
681
+
682
+ const resource = new class extends TestResource {
683
+ getSettings(): ResourceSettings<TestConfig> {
684
+ return {
685
+ id: 'resourceType',
686
+ operatingSystems: [OS.Darwin],
687
+ parameterSettings: {
688
+ propA: { type: 'directory' }
689
+ }
690
+ }
691
+ }
692
+
693
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
694
+ return { propA: testPath }
695
+ }
696
+ };
697
+
698
+ const controller = new ResourceController(resource);
699
+ const plan = await controller.plan({ type: 'resourceType' }, { propA: '~/test/folder' } as any, null, false);
700
+
701
+ expect(plan.changeSet.parameterChanges[0]).toMatchObject({
702
+ operation: ParameterOperation.NOOP,
703
+ newValue: testPath,
704
+ previousValue: testPath,
705
+ })
706
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
707
+ })
708
+
709
+ it('Ignores setting parameters when planning', async () => {
710
+ const resource = new class extends TestResource {
711
+ getSettings(): ResourceSettings<TestConfig> {
712
+ return {
713
+ id: 'resourceType',
714
+ operatingSystems: [OS.Darwin],
715
+ parameterSettings: {
716
+ propA: { type: 'string', setting: true },
717
+ propB: { type: 'number' }
718
+ }
719
+ }
720
+ }
721
+
722
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
723
+ return { propB: 64 }
724
+ }
725
+ };
726
+
727
+ const controller = new ResourceController(resource);
728
+ const plan = await controller.plan({ type: 'resourceType' }, { propA: 'setting', propB: 64 } as any, null, false);
729
+
730
+ expect(plan.changeSet.parameterChanges).toMatchObject(
731
+ expect.arrayContaining([
732
+ {
733
+ name: 'propA',
734
+ operation: ParameterOperation.NOOP,
735
+ previousValue: null,
736
+ newValue: 'setting',
737
+ isSensitive: false,
738
+ },
739
+ {
740
+ name: 'propB',
741
+ operation: ParameterOperation.NOOP,
742
+ previousValue: 64,
743
+ newValue: 64,
744
+ isSensitive: false,
745
+ }
746
+ ])
747
+ )
748
+
749
+ expect(plan.changeSet.operation).to.eq(ResourceOperation.NOOP);
750
+ })
751
+
752
+ it('Accepts an input parameters for imports', () => {
753
+ const resource = new class extends TestResource {
754
+ getSettings(): ResourceSettings<TestConfig> {
755
+ return {
756
+ id: 'resourceType',
757
+ operatingSystems: [OS.Darwin],
758
+ importAndDestroy: {
759
+ requiredParameters: ['propA'],
760
+ refreshKeys: ['propB', 'propA'],
761
+ defaultRefreshValues: {
762
+ propB: 6,
763
+ }
764
+ }
765
+ }
766
+ }
767
+ };
768
+ })
769
+
770
+ it('Accepts a string isEqual method which selects from one of the defaults', async () => {
771
+ const resource = new class extends TestResource {
772
+ getSettings(): ResourceSettings<TestConfig> {
773
+ return {
774
+ id: 'resourceType',
775
+ operatingSystems: [OS.Darwin],
776
+ parameterSettings: {
777
+ propA: { type: 'string', isEqual: 'version' }
778
+ }
779
+ }
780
+ }
781
+
782
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
783
+ return {
784
+ propA: '10.0.0'
785
+ }
786
+ }
787
+ };
788
+
789
+ const controller = new ResourceController(resource);
790
+
791
+ const result = await controller.plan({ type: 'resourceType' }, { propA: '10.0' }, null, false);
792
+ expect(result.changeSet).toMatchObject({
793
+ operation: ResourceOperation.NOOP,
794
+ })
795
+ });
796
+
797
+ it('Object equals method (works when equal)', async () => {
798
+ const resource = new class extends TestResource {
799
+ getSettings(): ResourceSettings<TestConfig> {
800
+ return {
801
+ id: 'resourceType',
802
+ operatingSystems: [OS.Darwin],
803
+ parameterSettings: {
804
+ propD: { type: 'object' }
805
+ }
806
+ }
807
+ }
808
+
809
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
810
+ return {
811
+ propD: {
812
+ testA: 'a',
813
+ testB: 'b',
814
+ testC: 10,
815
+ }
816
+ }
817
+ }
818
+ };
819
+
820
+ const controller = new ResourceController(resource);
821
+
822
+ const result = await controller.plan(
823
+ { type: 'resourceType' },
824
+ {
825
+ propD: {
826
+ testC: 10,
827
+ testA: 'a',
828
+ testB: 'b',
829
+ }
830
+ },
831
+ null,
832
+ false
833
+ );
834
+
835
+ expect(result.changeSet).toMatchObject({
836
+ operation: ResourceOperation.NOOP,
837
+ })
838
+ });
839
+
840
+ it('Object equals method (works when not equal)', async () => {
841
+ const resource = new class extends TestResource {
842
+ getSettings(): ResourceSettings<TestConfig> {
843
+ return {
844
+ id: 'resourceType',
845
+ operatingSystems: [OS.Darwin],
846
+ parameterSettings: {
847
+ propD: { type: 'object' }
848
+ }
849
+ }
850
+ }
851
+
852
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
853
+ return {
854
+ propD: {
855
+ testA: 'a',
856
+ testB: 'b',
857
+ }
858
+ }
859
+ }
860
+ };
861
+
862
+ const controller = new ResourceController(resource);
863
+
864
+ const result = await controller.plan(
865
+ { type: 'resourceType' },
866
+ {
867
+ propD: {
868
+ testC: 10,
869
+ testA: 'a',
870
+ testB: 'b',
871
+ }
872
+ },
873
+ null,
874
+ false
875
+ );
876
+
877
+ expect(result.changeSet).toMatchObject({
878
+ operation: ResourceOperation.RECREATE,
879
+ })
880
+ });
881
+
882
+ it('Transforms input parameters', async () => {
883
+ const resource = new class extends TestResource {
884
+ getSettings(): ResourceSettings<TestConfig> {
885
+ return {
886
+ id: 'resourceType',
887
+ operatingSystems: [OS.Darwin],
888
+ parameterSettings: {
889
+ propD: {
890
+ type: 'array',
891
+ transformation: {
892
+ to: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
893
+ Object.entries(h)
894
+ .map(([k, v]) => [
895
+ k,
896
+ typeof v === 'boolean'
897
+ ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
898
+ : v,
899
+ ])
900
+ )
901
+ ),
902
+ from: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
903
+ Object.entries(h)
904
+ .map(([k, v]) => [
905
+ k,
906
+ v === 'yes',
907
+ ])
908
+ ))
909
+ }
910
+ }
911
+ }
912
+ }
913
+ }
914
+
915
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
916
+ expect(parameters.propD[0].AddKeysToAgent).to.eq('yes')
917
+ expect(parameters.propD[1].AddKeysToAgent).to.eq('yes')
918
+ expect(parameters.propD[1].UseKeychain).to.eq('yes')
919
+ expect(parameters.propD[2].PasswordAuthentication).to.eq('yes')
920
+
921
+ return null;
922
+ }
923
+ }
924
+
925
+ const controller = new ResourceController(resource);
926
+ await controller.plan(
927
+ { type: 'resourceType' },
928
+ {
929
+ propD: [
930
+ {
931
+ Host: 'new.com',
932
+ AddKeysToAgent: true,
933
+ IdentityFile: 'id_ed25519'
934
+ },
935
+ {
936
+ Host: 'github.com',
937
+ AddKeysToAgent: true,
938
+ UseKeychain: true,
939
+ },
940
+ {
941
+ Match: 'User bob,joe,phil',
942
+ PasswordAuthentication: true,
943
+ }
944
+ ]
945
+ },
946
+ null,
947
+ false
948
+ );
949
+
950
+ })
951
+
952
+ it('Transforms input parameters for stateful parameters', async () => {
953
+ const sp = new class extends TestStatefulParameter {
954
+ getSettings(): any {
955
+ return {
956
+ type: 'array',
957
+ transformation: {
958
+ to: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
959
+ Object.entries(h)
960
+ .map(([k, v]) => [
961
+ k,
962
+ typeof v === 'boolean'
963
+ ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans
964
+ : v,
965
+ ])
966
+ )
967
+ ),
968
+ from: (hosts: Record<string, unknown>[]) => hosts.map((h) => Object.fromEntries(
969
+ Object.entries(h)
970
+ .map(([k, v]) => [
971
+ k,
972
+ v === 'yes',
973
+ ])
974
+ ))
975
+ }
976
+ }
977
+ }
978
+
979
+ async refresh(desired: any): Promise<any | null> {
980
+ expect(desired[0].AddKeysToAgent).to.eq('yes')
981
+ expect(desired[1].AddKeysToAgent).to.eq('yes')
982
+ expect(desired[1].UseKeychain).to.eq('yes')
983
+ expect(desired[2].PasswordAuthentication).to.eq('yes')
984
+
985
+ return null;
986
+ }
987
+ }
988
+
989
+ const resource = new class extends TestResource {
990
+ getSettings(): ResourceSettings<TestConfig> {
991
+ return {
992
+ id: 'resourceType',
993
+ operatingSystems: [OS.Darwin],
994
+ parameterSettings: {
995
+ propD: { type: 'stateful', definition: sp }
996
+ }
997
+ }
998
+ }
999
+ }
1000
+
1001
+ const controller = new ResourceController(resource);
1002
+ await controller.plan(
1003
+ { type: 'resourceType' },
1004
+ {
1005
+ propD: [
1006
+ {
1007
+ Host: 'new.com',
1008
+ AddKeysToAgent: true,
1009
+ IdentityFile: 'id_ed25519'
1010
+ },
1011
+ {
1012
+ Host: 'github.com',
1013
+ AddKeysToAgent: true,
1014
+ UseKeychain: true,
1015
+ },
1016
+ {
1017
+ Match: 'User bob,joe,phil',
1018
+ PasswordAuthentication: true,
1019
+ }
1020
+ ]
1021
+ },
1022
+ null,
1023
+ false
1024
+ );
1025
+
1026
+ })
1027
+
1028
+ it('Supports equality check for itemType', async () => {
1029
+ const resource = new class extends TestResource {
1030
+ getSettings(): ResourceSettings<TestConfig> {
1031
+ return {
1032
+ id: 'resourceType',
1033
+ operatingSystems: [OS.Darwin],
1034
+ parameterSettings: {
1035
+ propA: { type: 'array', itemType: 'version' }
1036
+ }
1037
+ }
1038
+ }
1039
+
1040
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
1041
+ return {
1042
+ propA: ['10.0.0']
1043
+ }
1044
+ }
1045
+ };
1046
+
1047
+ const controller = new ResourceController(resource);
1048
+
1049
+ const result = await controller.plan({ type: 'resourceType' }, { propA: ['10.0'] }, null, false);
1050
+ expect(result.changeSet).toMatchObject({
1051
+ operation: ResourceOperation.NOOP,
1052
+ })
1053
+ })
1054
+
1055
+ it('Supports transformations for itemType', async () => {
1056
+ const home = os.homedir()
1057
+ const testPath = path.join(home, 'test/folder');
1058
+
1059
+ const resource = new class extends TestResource {
1060
+ getSettings(): ResourceSettings<TestConfig> {
1061
+ return {
1062
+ id: 'resourceType',
1063
+ operatingSystems: [OS.Darwin],
1064
+ parameterSettings: {
1065
+ propA: { type: 'array', itemType: 'directory' }
1066
+ }
1067
+ }
1068
+ }
1069
+
1070
+ async refresh(parameters: Partial<TestConfig>): Promise<Partial<TestConfig> | null> {
1071
+ return {
1072
+ propA: [testPath]
1073
+ }
1074
+ }
1075
+ };
1076
+
1077
+ const controller = new ResourceController(resource);
1078
+
1079
+ const result = await controller.plan({ type: 'resourceType' }, { propA: ['~/test/folder'] }, null, false);
1080
+ expect(result.changeSet).toMatchObject({
1081
+ operation: ResourceOperation.NOOP,
1082
+ })
1083
+ })
1084
+
1085
+ it('Supports matching using the identfying parameters', async () => {
1086
+ const home = os.homedir()
1087
+ const testPath = path.join(home, 'test/folder');
1088
+
1089
+ const resource = new class extends TestResource {
1090
+ getSettings(): ResourceSettings<TestConfig> {
1091
+ return {
1092
+ id: 'resourceType',
1093
+ operatingSystems: [OS.Darwin],
1094
+ parameterSettings: {
1095
+ propA: { type: 'array', itemType: 'directory' }
1096
+ },
1097
+ allowMultiple: {
1098
+ identifyingParameters: ['propA']
1099
+ }
1100
+ }
1101
+ }
1102
+ };
1103
+
1104
+ const controller = new ResourceController(resource);
1105
+ expect(controller.parsedSettings.matcher({
1106
+ propA: [testPath],
1107
+ propB: 'random1',
1108
+ }, {
1109
+ propA: [testPath],
1110
+ propB: 'random2',
1111
+ })).to.be.true;
1112
+
1113
+ expect(controller.parsedSettings.matcher({
1114
+ propA: [testPath],
1115
+ propB: 'random1',
1116
+ }, {
1117
+ propA: [testPath, testPath],
1118
+ propB: 'random2',
1119
+ })).to.be.false;
1120
+ })
1121
+
1122
+ it('Supports matching using custom matcher', async () => {
1123
+ const home = os.homedir()
1124
+ const testPath = path.join(home, 'test/folder');
1125
+
1126
+ const resource = new class extends TestResource {
1127
+ getSettings(): ResourceSettings<TestConfig> {
1128
+ return {
1129
+ id: 'resourceType',
1130
+ operatingSystems: [OS.Darwin],
1131
+ parameterSettings: {
1132
+ propA: { type: 'array', itemType: 'directory' }
1133
+ },
1134
+ allowMultiple: {
1135
+ identifyingParameters: ['propA'],
1136
+ matcher: () => false,
1137
+ }
1138
+ }
1139
+ }
1140
+ };
1141
+
1142
+ const controller = new ResourceController(resource);
1143
+ expect(controller.parsedSettings.matcher({
1144
+ propA: [testPath],
1145
+ propB: 'random1',
1146
+ }, {
1147
+ propA: [testPath],
1148
+ propB: 'random2',
1149
+ })).to.be.false;
1150
+ })
1151
+
1152
+ it('Can match directories 1', async () => {
1153
+ const resource = new class extends TestResource {
1154
+ getSettings(): ResourceSettings<TestConfig> {
1155
+ return {
1156
+ id: 'resourceType',
1157
+ operatingSystems: [OS.Darwin],
1158
+ parameterSettings: {
1159
+ propA: { type: 'directory' }
1160
+ },
1161
+ }
1162
+ }
1163
+ };
1164
+
1165
+ const controller = new ResourceController(resource);
1166
+ const transformations = controller.parsedSettings.inputTransformations.propA;
1167
+
1168
+ const to = transformations!.to('$HOME/abc/def')
1169
+ expect(to).to.eq(os.homedir() + '/abc/def')
1170
+
1171
+ const from = transformations!.from(os.homedir() + '/abc/def')
1172
+ expect(from).to.eq('~/abc/def')
1173
+
1174
+ const from2 = transformations!.from(os.homedir() + '/abc/def', '$HOME/abc/def')
1175
+ expect(from2).to.eq('$HOME/abc/def')
1176
+
1177
+ })
1178
+
1179
+ it('Can match directories 2', async () => {
1180
+
1181
+ const schema = z.object({
1182
+ propA: z.string(),
1183
+ propB: z.number(),
1184
+ });
1185
+
1186
+ const resource = new class extends TestResource {
1187
+ getSettings(): ResourceSettings<z.infer<typeof schema>> {
1188
+ return {
1189
+ id: 'resourceType',
1190
+ schema,
1191
+ operatingSystems: [OS.Darwin],
1192
+ parameterSettings: {
1193
+ propA: { type: 'directory' }
1194
+ },
1195
+ }
1196
+ }
1197
+ };
1198
+
1199
+ const controller = new ResourceController(resource);
1200
+ const transformations = controller.parsedSettings.inputTransformations.propA;
1201
+
1202
+ const to = transformations!.to('$HOME/abc/def')
1203
+ expect(to).to.eq(os.homedir() + '/abc/def')
1204
+
1205
+ const from = transformations!.from(os.homedir() + '/abc/def')
1206
+ expect(from).to.eq('~/abc/def')
1207
+
1208
+ const from2 = transformations!.from(os.homedir() + '/abc/def', '$HOME/abc/def')
1209
+ expect(from2).to.eq('$HOME/abc/def')
1210
+
1211
+ })
1212
+
1213
+ })