codify-plugin-lib 1.0.37 → 1.0.39

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.
@@ -1,87 +1,162 @@
1
- import { ParameterOperation, ResourceOperation } from 'codify-schemas';
2
- import { ChangeSet } from './change-set.js';
1
+ import { ParameterOperation, ResourceOperation, } from 'codify-schemas';
3
2
  import { Plan } from './plan.js';
3
+ import { setsEqual, splitUserConfig } from '../utils/utils.js';
4
4
  export class Resource {
5
+ typeId;
6
+ statefulParameters;
5
7
  dependencies;
6
- statefulParameters = new Map();
7
- constructor(dependencies = []) {
8
- this.dependencies = dependencies;
8
+ parameterConfigurations;
9
+ options;
10
+ constructor(configuration) {
11
+ this.validateResourceConfiguration(configuration);
12
+ this.typeId = configuration.type;
13
+ this.statefulParameters = new Map(configuration.statefulParameters?.map((sp) => [sp.name, sp]));
14
+ this.parameterConfigurations = this.generateParameterConfigurations(configuration);
15
+ this.dependencies = configuration.dependencies ?? [];
16
+ this.options = configuration;
9
17
  }
10
18
  getDependencyTypeIds() {
11
- return this.dependencies.map((d) => d.getTypeId());
19
+ return this.dependencies.map((d) => d.typeId);
12
20
  }
13
21
  async onInitialize() { }
14
22
  async plan(desiredConfig) {
15
- const currentConfig = await this.getCurrentConfig(desiredConfig);
16
- if (!currentConfig) {
17
- return Plan.create(ChangeSet.createForNullCurrentConfig(desiredConfig), desiredConfig);
23
+ const planConfiguration = {
24
+ statefulMode: false,
25
+ parameterConfigurations: this.parameterConfigurations,
26
+ };
27
+ const { resourceMetadata, parameters: desiredParameters } = splitUserConfig(desiredConfig);
28
+ const resourceParameters = Object.fromEntries([
29
+ ...Object.entries(desiredParameters).filter(([key]) => !this.statefulParameters.has(key)),
30
+ ]);
31
+ const statefulParameters = [...this.statefulParameters.values()]
32
+ .filter((sp) => desiredParameters[sp.name] !== undefined);
33
+ const keysToRefresh = new Set(Object.keys(resourceParameters));
34
+ const currentParameters = await this.refresh(keysToRefresh);
35
+ if (currentParameters == null && statefulParameters.length === 0) {
36
+ return Plan.create(desiredConfig, null, planConfiguration);
18
37
  }
19
- const desiredConfigStatefulParameters = [...this.statefulParameters.values()]
20
- .filter((sp) => desiredConfig[sp.name] !== undefined);
21
- for (const statefulParameter of desiredConfigStatefulParameters) {
22
- const parameterCurrentStatus = await statefulParameter.getCurrent(desiredConfig[statefulParameter.name]);
23
- if (parameterCurrentStatus) {
24
- currentConfig[statefulParameter.name] = parameterCurrentStatus;
38
+ this.validateRefreshResults(currentParameters, keysToRefresh);
39
+ const currentStatefulParameters = {};
40
+ for (const statefulParameter of statefulParameters) {
41
+ const desiredValue = desiredParameters[statefulParameter.name];
42
+ let currentValue = await statefulParameter.refresh(desiredValue ?? null) ?? undefined;
43
+ if (Array.isArray(currentValue) && Array.isArray(desiredValue) && !planConfiguration.statefulMode) {
44
+ currentValue = currentValue.filter((p) => desiredValue?.includes(p));
25
45
  }
46
+ currentStatefulParameters[statefulParameter.name] = currentValue;
26
47
  }
27
- const parameterChangeSet = ChangeSet.calculateParameterChangeSet(currentConfig, desiredConfig);
28
- const resourceOperation = parameterChangeSet
29
- .filter((change) => change.operation !== ParameterOperation.NOOP)
30
- .reduce((operation, curr) => {
31
- const newOperation = !this.statefulParameters.has(curr.name)
32
- ? this.calculateOperation(curr)
33
- : ResourceOperation.MODIFY;
34
- return ChangeSet.combineResourceOperations(operation, newOperation);
35
- }, ResourceOperation.NOOP);
36
- return Plan.create(new ChangeSet(resourceOperation, parameterChangeSet), desiredConfig);
48
+ return Plan.create(desiredConfig, { ...currentParameters, ...currentStatefulParameters, ...resourceMetadata }, planConfiguration);
37
49
  }
38
50
  async apply(plan) {
39
- if (plan.getResourceType() !== this.getTypeId()) {
40
- throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.getTypeId()} but got: ${plan.getResourceType()}`);
51
+ if (plan.getResourceType() !== this.typeId) {
52
+ throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`);
41
53
  }
42
54
  switch (plan.changeSet.operation) {
55
+ case ResourceOperation.CREATE: {
56
+ return this._applyCreate(plan);
57
+ }
43
58
  case ResourceOperation.MODIFY: {
44
- const parameterChanges = plan.changeSet.parameterChanges
45
- .filter((c) => c.operation !== ParameterOperation.NOOP);
46
- const statelessParameterChanges = parameterChanges.filter((pc) => !this.statefulParameters.has(pc.name));
47
- if (statelessParameterChanges.length > 0) {
48
- await this.applyModify(plan);
59
+ return this._applyModify(plan);
60
+ }
61
+ case ResourceOperation.RECREATE: {
62
+ await this._applyDestroy(plan);
63
+ return this._applyCreate(plan);
64
+ }
65
+ case ResourceOperation.DESTROY: {
66
+ return this._applyDestroy(plan);
67
+ }
68
+ }
69
+ }
70
+ async _applyCreate(plan) {
71
+ await this.applyCreate(plan);
72
+ const statefulParameterChanges = plan.changeSet.parameterChanges
73
+ .filter((pc) => this.statefulParameters.has(pc.name));
74
+ for (const parameterChange of statefulParameterChanges) {
75
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
76
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
77
+ }
78
+ }
79
+ async _applyModify(plan) {
80
+ const parameterChanges = plan
81
+ .changeSet
82
+ .parameterChanges
83
+ .filter((c) => c.operation !== ParameterOperation.NOOP);
84
+ const statelessParameterChanges = parameterChanges
85
+ .filter((pc) => !this.statefulParameters.has(pc.name));
86
+ for (const pc of statelessParameterChanges) {
87
+ await this.applyModify(pc.name, pc.newValue, pc.previousValue, false, plan);
88
+ }
89
+ const statefulParameterChanges = parameterChanges
90
+ .filter((pc) => this.statefulParameters.has(pc.name));
91
+ for (const parameterChange of statefulParameterChanges) {
92
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
93
+ switch (parameterChange.operation) {
94
+ case ParameterOperation.ADD: {
95
+ await statefulParameter.applyAdd(parameterChange.newValue, plan);
96
+ break;
49
97
  }
50
- const statefulParameterChanges = parameterChanges.filter((pc) => this.statefulParameters.has(pc.name));
51
- for (const parameterChange of statefulParameterChanges) {
52
- const statefulParameter = this.statefulParameters.get(parameterChange.name);
53
- switch (parameterChange.operation) {
54
- case ParameterOperation.ADD: {
55
- await statefulParameter.applyAdd(parameterChange, plan);
56
- break;
57
- }
58
- case ParameterOperation.MODIFY: {
59
- await statefulParameter.applyModify(parameterChange, plan);
60
- break;
61
- }
62
- case ParameterOperation.REMOVE: {
63
- await statefulParameter.applyRemove(parameterChange, plan);
64
- break;
65
- }
66
- }
98
+ case ParameterOperation.MODIFY: {
99
+ await statefulParameter.applyModify(parameterChange.newValue, parameterChange.previousValue, false, plan);
100
+ break;
101
+ }
102
+ case ParameterOperation.REMOVE: {
103
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
104
+ break;
67
105
  }
68
- return;
69
106
  }
70
- case ResourceOperation.CREATE: {
71
- await this.applyCreate(plan);
72
- const statefulParameterChanges = plan.changeSet.parameterChanges
73
- .filter((pc) => this.statefulParameters.has(pc.name));
74
- for (const parameterChange of statefulParameterChanges) {
75
- const statefulParameter = this.statefulParameters.get(parameterChange.name);
76
- await statefulParameter.applyAdd(parameterChange, plan);
107
+ }
108
+ }
109
+ async _applyDestroy(plan) {
110
+ if (this.options.callStatefulParameterRemoveOnDestroy) {
111
+ const statefulParameterChanges = plan.changeSet.parameterChanges
112
+ .filter((pc) => this.statefulParameters.has(pc.name));
113
+ for (const parameterChange of statefulParameterChanges) {
114
+ const statefulParameter = this.statefulParameters.get(parameterChange.name);
115
+ await statefulParameter.applyRemove(parameterChange.previousValue, plan);
116
+ }
117
+ }
118
+ await this.applyDestroy(plan);
119
+ }
120
+ generateParameterConfigurations(resourceConfiguration) {
121
+ const resourceParameters = Object.fromEntries(Object.entries(resourceConfiguration.parameterConfigurations ?? {})
122
+ ?.map(([name, value]) => ([name, { ...value, isStatefulParameter: false }])));
123
+ const statefulParameters = resourceConfiguration.statefulParameters
124
+ ?.reduce((obj, sp) => {
125
+ return {
126
+ ...obj,
127
+ [sp.name]: {
128
+ ...sp.configuration,
129
+ isStatefulParameter: true,
77
130
  }
78
- return;
131
+ };
132
+ }, {}) ?? {};
133
+ return {
134
+ ...resourceParameters,
135
+ ...statefulParameters,
136
+ };
137
+ }
138
+ validateResourceConfiguration(data) {
139
+ if (data.parameterConfigurations && data.statefulParameters) {
140
+ const parameters = [...Object.keys(data.parameterConfigurations)];
141
+ const statefulParameterSet = new Set(Object.keys(data.statefulParameters));
142
+ const intersection = parameters.some((p) => statefulParameterSet.has(p));
143
+ if (intersection) {
144
+ throw new Error(`Resource ${this.typeId} cannot declare a parameter as both stateful and non-stateful`);
79
145
  }
80
- case ResourceOperation.RECREATE: return this.applyRecreate(plan);
81
- case ResourceOperation.DESTROY: return this.applyDestroy(plan);
82
146
  }
83
147
  }
84
- registerStatefulParameter(parameter) {
85
- this.statefulParameters.set(parameter.name, parameter);
148
+ validateRefreshResults(refresh, desiredKeys) {
149
+ if (!refresh) {
150
+ return;
151
+ }
152
+ const refreshKeys = new Set(Object.keys(refresh));
153
+ if (!setsEqual(desiredKeys, refreshKeys)) {
154
+ throw new Error(`Resource ${this.options.type}
155
+ refresh() must return back exactly the keys that were provided
156
+ Missing: ${[...desiredKeys].filter((k) => !refreshKeys.has(k))};
157
+ Additional: ${[...refreshKeys].filter(k => !desiredKeys.has(k))};`);
158
+ }
86
159
  }
160
+ async applyModify(parameterName, newValue, previousValue, allowDeletes, plan) { }
161
+ ;
87
162
  }
@@ -1,10 +1,24 @@
1
- import { ParameterChange } from './change-set.js';
2
1
  import { Plan } from './plan.js';
3
- import { ResourceConfig } from 'codify-schemas';
4
- export declare abstract class StatefulParameter<T extends ResourceConfig, K extends keyof T> {
5
- abstract get name(): K;
6
- abstract getCurrent(desiredValue: T[K]): Promise<T[K]>;
7
- abstract applyAdd(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
8
- abstract applyModify(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
9
- abstract applyRemove(parameterChange: ParameterChange, plan: Plan<T>): Promise<void>;
2
+ import { StringIndexedObject } from 'codify-schemas';
3
+ export interface StatefulParameterConfiguration<T> {
4
+ name: keyof T;
5
+ isEqual?: (a: any, b: any) => boolean;
6
+ }
7
+ export declare abstract class StatefulParameter<T extends StringIndexedObject, V extends T[keyof T]> {
8
+ readonly name: keyof T;
9
+ readonly configuration: StatefulParameterConfiguration<T>;
10
+ protected constructor(configuration: StatefulParameterConfiguration<T>);
11
+ abstract refresh(previousValue: V | null): Promise<V | null>;
12
+ abstract applyAdd(valueToAdd: V, plan: Plan<T>): Promise<void>;
13
+ abstract applyModify(newValue: V, previousValue: V, allowDeletes: boolean, plan: Plan<T>): Promise<void>;
14
+ abstract applyRemove(valueToRemove: V, plan: Plan<T>): Promise<void>;
15
+ }
16
+ export declare abstract class ArrayStatefulParameter<T extends StringIndexedObject, V> extends StatefulParameter<T, any> {
17
+ protected constructor(configuration: StatefulParameterConfiguration<T>);
18
+ applyAdd(valuesToAdd: V[], plan: Plan<T>): Promise<void>;
19
+ applyModify(newValues: V[], previousValues: V[], allowDeletes: boolean, plan: Plan<T>): Promise<void>;
20
+ applyRemove(valuesToRemove: V[], plan: Plan<T>): Promise<void>;
21
+ abstract refresh(previousValue: V[] | null): Promise<V[] | null>;
22
+ abstract applyAddItem(item: V, plan: Plan<T>): Promise<void>;
23
+ abstract applyRemoveItem(item: V, plan: Plan<T>): Promise<void>;
10
24
  }
@@ -1,2 +1,35 @@
1
1
  export class StatefulParameter {
2
+ name;
3
+ configuration;
4
+ constructor(configuration) {
5
+ this.name = configuration.name;
6
+ this.configuration = configuration;
7
+ }
8
+ }
9
+ export class ArrayStatefulParameter extends StatefulParameter {
10
+ constructor(configuration) {
11
+ super(configuration);
12
+ }
13
+ async applyAdd(valuesToAdd, plan) {
14
+ for (const value of valuesToAdd) {
15
+ await this.applyAddItem(value, plan);
16
+ }
17
+ }
18
+ async applyModify(newValues, previousValues, allowDeletes, plan) {
19
+ const valuesToAdd = newValues.filter((n) => !previousValues.includes(n));
20
+ const valuesToRemove = previousValues.filter((n) => !newValues.includes(n));
21
+ for (const value of valuesToAdd) {
22
+ await this.applyAddItem(value, plan);
23
+ }
24
+ if (allowDeletes) {
25
+ for (const value of valuesToRemove) {
26
+ await this.applyRemoveItem(value, plan);
27
+ }
28
+ }
29
+ }
30
+ async applyRemove(valuesToRemove, plan) {
31
+ for (const value of valuesToRemove) {
32
+ await this.applyRemoveItem(value, plan);
33
+ }
34
+ }
2
35
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { Resource } from './resource.js';
2
+ class Test extends Resource {
3
+ validate(config) {
4
+ throw new Error('Method not implemented.');
5
+ }
6
+ async refresh(keys) {
7
+ const result = {};
8
+ if (keys.has('propA')) {
9
+ result['propA'] = 'abc';
10
+ }
11
+ return result;
12
+ }
13
+ applyCreate(plan) {
14
+ throw new Error('Method not implemented.');
15
+ }
16
+ applyModify(parameterName, newValue, previousValue, plan) {
17
+ throw new Error('Method not implemented.');
18
+ }
19
+ applyDestroy(plan) {
20
+ throw new Error('Method not implemented.');
21
+ }
22
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import { Plugin } from './entities/plugin.js';
2
2
  export * from './entities/resource.js';
3
+ export * from './entities/resource-types.js';
3
4
  export * from './entities/plugin.js';
4
5
  export * from './entities/change-set.js';
5
6
  export * from './entities/plan.js';
7
+ export * from './entities/plan-types.js';
6
8
  export * from './entities/stateful-parameter.js';
7
9
  export * from './utils/test-utils.js';
8
10
  export * from './utils/utils.js';
9
11
  export declare function runPlugin(plugin: Plugin): Promise<void>;
12
+ export { ErrorMessage } from './entities/resource-types.js';
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { MessageHandler } from './messages/handlers.js';
2
2
  export * from './entities/resource.js';
3
+ export * from './entities/resource-types.js';
3
4
  export * from './entities/plugin.js';
4
5
  export * from './entities/change-set.js';
5
6
  export * from './entities/plan.js';
7
+ export * from './entities/plan-types.js';
6
8
  export * from './entities/stateful-parameter.js';
7
9
  export * from './utils/test-utils.js';
8
10
  export * from './utils/utils.js';
@@ -0,0 +1,3 @@
1
+ export interface StringIndexedObject {
2
+ [x: string]: unknown;
3
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
2
  import { SpawnOptions } from 'child_process';
3
+ import { ResourceConfig, StringIndexedObject } from 'codify-schemas';
3
4
  export declare enum SpawnStatus {
4
5
  SUCCESS = "success",
5
6
  ERROR = "error"
@@ -16,4 +17,9 @@ export declare function codifySpawn(cmd: string, args?: string[], opts?: Omit<Co
16
17
  throws?: boolean;
17
18
  }, extras?: Record<any, any>): Promise<SpawnResult>;
18
19
  export declare function isDebug(): boolean;
20
+ export declare function splitUserConfig<T extends StringIndexedObject>(config: T & ResourceConfig): {
21
+ parameters: T;
22
+ resourceMetadata: ResourceConfig;
23
+ };
24
+ export declare function setsEqual(set1: Set<unknown>, set2: Set<unknown>): boolean;
19
25
  export {};
@@ -36,3 +36,18 @@ export async function codifySpawn(cmd, args, opts, extras) {
36
36
  export function isDebug() {
37
37
  return process.env.DEBUG != null && process.env.DEBUG.includes('codify');
38
38
  }
39
+ export function splitUserConfig(config) {
40
+ const resourceMetadata = {
41
+ type: config.type,
42
+ ...(config.name && { name: config.name }),
43
+ ...(config.dependsOn && { dependsOn: config.dependsOn }),
44
+ };
45
+ const { type, name, dependsOn, ...parameters } = config;
46
+ return {
47
+ parameters: parameters,
48
+ resourceMetadata,
49
+ };
50
+ }
51
+ export function setsEqual(set1, set2) {
52
+ return set1.size === set2.size && [...set1].every((v) => set2.has(v));
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codify-plugin-lib",
3
- "version": "1.0.37",
3
+ "version": "1.0.39",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  "dependencies": {
15
15
  "ajv": "^8.12.0",
16
16
  "ajv-formats": "^2.1.1",
17
- "codify-schemas": "1.0.30",
17
+ "codify-schemas": "1.0.32",
18
18
  "@npmcli/promise-spawn": "^7.0.1"
19
19
  },
20
20
  "devDependencies": {
@@ -1,107 +1,95 @@
1
- import { ChangeSet } from './change-set';
1
+ import { ChangeSet } from './change-set.js';
2
2
  import { ParameterOperation, ResourceOperation } from 'codify-schemas';
3
- import { describe, it, expect } from 'vitest';
3
+ import { describe, expect, it } from 'vitest';
4
4
 
5
- describe('Change set tests', () => {
5
+ describe('Change set tests (stateful)', () => {
6
6
  it ('Correctly diffs two resource configs (modify)', () => {
7
- const before = {
8
- type: 'config',
7
+ const after = {
9
8
  propA: 'before',
10
9
  propB: 'before'
11
10
  }
12
11
 
13
- const after = {
14
- type: 'config',
12
+ const before = {
15
13
  propA: 'after',
16
14
  propB: 'after'
17
15
  }
18
16
 
19
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
17
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
20
18
  expect(cs.length).to.eq(2);
21
19
  expect(cs[0].operation).to.eq(ParameterOperation.MODIFY);
22
20
  expect(cs[1].operation).to.eq(ParameterOperation.MODIFY);
23
21
  })
24
22
 
25
23
  it ('Correctly diffs two resource configs (add)', () => {
26
- const before = {
27
- type: 'config',
24
+ const after = {
28
25
  propA: 'before',
26
+ propB: 'after'
29
27
  }
30
28
 
31
- const after = {
32
- type: 'config',
29
+ const before = {
33
30
  propA: 'after',
34
- propB: 'after'
35
31
  }
36
32
 
37
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
33
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
38
34
  expect(cs.length).to.eq(2);
39
35
  expect(cs[0].operation).to.eq(ParameterOperation.MODIFY);
40
36
  expect(cs[1].operation).to.eq(ParameterOperation.ADD);
41
37
  })
42
38
 
43
39
  it ('Correctly diffs two resource configs (remove)', () => {
40
+ const after = {
41
+ propA: 'after',
42
+ }
43
+
44
44
  const before = {
45
- type: 'config',
46
45
  propA: 'before',
47
46
  propB: 'before'
48
47
  }
49
48
 
50
- const after = {
51
- type: 'config',
52
- propA: 'after',
53
- }
54
-
55
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
49
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
56
50
  expect(cs.length).to.eq(2);
57
51
  expect(cs[0].operation).to.eq(ParameterOperation.MODIFY);
58
52
  expect(cs[1].operation).to.eq(ParameterOperation.REMOVE);
59
53
  })
60
54
 
61
55
  it ('Correctly diffs two resource configs (no-op)', () => {
62
- const before = {
63
- type: 'config',
56
+ const after = {
64
57
  propA: 'prop',
65
58
  }
66
59
 
67
- const after = {
68
- type: 'config',
60
+ const before = {
69
61
  propA: 'prop',
70
62
  }
71
63
 
72
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
64
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
73
65
  expect(cs.length).to.eq(1);
74
66
  expect(cs[0].operation).to.eq(ParameterOperation.NOOP);
75
67
  })
76
68
 
77
69
  it ('handles simple arrays', () => {
78
70
  const before = {
79
- type: 'config',
80
71
  propA: ['a', 'b', 'c'],
81
72
  }
82
73
 
83
74
  const after = {
84
- type: 'config',
85
75
  propA: ['b', 'a', 'c'],
86
76
  }
87
77
 
88
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
78
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
89
79
  expect(cs.length).to.eq(1);
90
80
  expect(cs[0].operation).to.eq(ParameterOperation.NOOP);
91
81
  })
92
82
 
93
83
  it ('handles simple arrays', () => {
94
- const before = {
95
- type: 'config',
96
- propA: ['a', 'b'],
84
+ const after = {
85
+ propA: ['a', 'b', 'c'],
97
86
  }
98
87
 
99
- const after = {
100
- type: 'config',
101
- propA: ['b', 'a', 'c'],
88
+ const before = {
89
+ propA: ['b', 'a'],
102
90
  }
103
91
 
104
- const cs = ChangeSet.calculateParameterChangeSet(before, after);
92
+ const cs = ChangeSet.calculateParameterChangeSet(after, before, { statefulMode: true });
105
93
  expect(cs.length).to.eq(1);
106
94
  expect(cs[0].operation).to.eq(ParameterOperation.MODIFY);
107
95
  })