@featurevisor/core 2.13.0 → 2.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/coverage/clover.xml +906 -557
  3. package/coverage/coverage-final.json +11 -8
  4. package/coverage/lcov-report/builder/allocator.ts.html +1 -1
  5. package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
  6. package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
  7. package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
  8. package/coverage/lcov-report/builder/index.html +38 -8
  9. package/coverage/lcov-report/builder/mutateVariables.ts.html +400 -0
  10. package/coverage/lcov-report/builder/mutator.ts.html +796 -0
  11. package/coverage/lcov-report/builder/revision.ts.html +1 -1
  12. package/coverage/lcov-report/builder/traffic.ts.html +1 -1
  13. package/coverage/lcov-report/config/index.html +1 -1
  14. package/coverage/lcov-report/config/projectConfig.ts.html +1 -1
  15. package/coverage/lcov-report/datasource/adapter.ts.html +1 -1
  16. package/coverage/lcov-report/datasource/datasource.ts.html +4 -4
  17. package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +23 -23
  18. package/coverage/lcov-report/datasource/index.html +1 -1
  19. package/coverage/lcov-report/datasource/index.ts.html +1 -1
  20. package/coverage/lcov-report/index.html +28 -28
  21. package/coverage/lcov-report/linter/attributeSchema.ts.html +1 -1
  22. package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
  23. package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
  24. package/coverage/lcov-report/linter/conditionSchema.ts.html +7 -7
  25. package/coverage/lcov-report/linter/featureSchema.ts.html +789 -318
  26. package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
  27. package/coverage/lcov-report/linter/index.html +34 -19
  28. package/coverage/lcov-report/linter/lintProject.ts.html +25 -25
  29. package/coverage/lcov-report/linter/mutationNotation.ts.html +1309 -0
  30. package/coverage/lcov-report/linter/printError.ts.html +1 -1
  31. package/coverage/lcov-report/linter/schema.ts.html +69 -69
  32. package/coverage/lcov-report/linter/segmentSchema.ts.html +1 -1
  33. package/coverage/lcov-report/linter/testSchema.ts.html +5 -5
  34. package/coverage/lcov-report/list/index.html +1 -1
  35. package/coverage/lcov-report/list/matrix.ts.html +1 -1
  36. package/coverage/lcov-report/parsers/index.html +1 -1
  37. package/coverage/lcov-report/parsers/index.ts.html +1 -1
  38. package/coverage/lcov-report/parsers/json.ts.html +1 -1
  39. package/coverage/lcov-report/parsers/yml.ts.html +2 -2
  40. package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
  41. package/coverage/lcov-report/tester/helpers.ts.html +1 -1
  42. package/coverage/lcov-report/tester/index.html +1 -1
  43. package/coverage/lcov-report/utils/git.ts.html +1 -1
  44. package/coverage/lcov-report/utils/index.html +1 -1
  45. package/coverage/lcov.info +1839 -1120
  46. package/lib/builder/buildDatafile.js +8 -3
  47. package/lib/builder/buildDatafile.js.map +1 -1
  48. package/lib/builder/mutateVariables.d.ts +14 -0
  49. package/lib/builder/mutateVariables.js +90 -0
  50. package/lib/builder/mutateVariables.js.map +1 -0
  51. package/lib/builder/mutateVariables.spec.d.ts +1 -0
  52. package/lib/builder/mutateVariables.spec.js +1045 -0
  53. package/lib/builder/mutateVariables.spec.js.map +1 -0
  54. package/lib/builder/mutator.d.ts +20 -0
  55. package/lib/builder/mutator.js +223 -0
  56. package/lib/builder/mutator.js.map +1 -0
  57. package/lib/builder/mutator.spec.d.ts +1 -0
  58. package/lib/builder/mutator.spec.js +368 -0
  59. package/lib/builder/mutator.spec.js.map +1 -0
  60. package/lib/linter/featureSchema.d.ts +7 -7
  61. package/lib/linter/featureSchema.js +138 -62
  62. package/lib/linter/featureSchema.js.map +1 -1
  63. package/lib/linter/featureSchema.spec.js +150 -0
  64. package/lib/linter/featureSchema.spec.js.map +1 -1
  65. package/lib/linter/mutationNotation.d.ts +47 -0
  66. package/lib/linter/mutationNotation.js +381 -0
  67. package/lib/linter/mutationNotation.js.map +1 -0
  68. package/lib/linter/mutationNotation.spec.d.ts +1 -0
  69. package/lib/linter/mutationNotation.spec.js +549 -0
  70. package/lib/linter/mutationNotation.spec.js.map +1 -0
  71. package/package.json +2 -2
  72. package/src/builder/buildDatafile.ts +29 -3
  73. package/src/builder/mutateVariables.spec.ts +1134 -0
  74. package/src/builder/mutateVariables.ts +105 -0
  75. package/src/builder/mutator.spec.ts +413 -0
  76. package/src/builder/mutator.ts +237 -0
  77. package/src/linter/featureSchema.spec.ts +182 -0
  78. package/src/linter/featureSchema.ts +254 -97
  79. package/src/linter/mutationNotation.spec.ts +642 -0
  80. package/src/linter/mutationNotation.ts +408 -0
@@ -0,0 +1,105 @@
1
+ import type { VariableSchema, VariableValue } from "@featurevisor/types";
2
+ import { mutate } from "./mutator";
3
+
4
+ const MUTATION_OP_SUFFIX = /:((?:append|prepend|after|before|remove))$/;
5
+
6
+ /**
7
+ * Get the root variable name from an override key (e.g. "tags:append" -> "tags", "payload.rows:append" -> "payload").
8
+ */
9
+ function rootVariableFromOverrideKey(overrideKey: string): string {
10
+ const withoutSuffix = overrideKey.replace(MUTATION_OP_SUFFIX, "").trim();
11
+ const firstSegment = withoutSuffix.includes(".") ? withoutSuffix.split(".")[0] : withoutSuffix;
12
+ return firstSegment.replace(/\s*\[.*\]\s*$/, "").trim();
13
+ }
14
+
15
+ /**
16
+ * Resolve variable values from schema defaults and overrides.
17
+ * Override keys may be variable keys or dot-notation paths (e.g. "foo", "foo.a.b", "tags:append", "items[id=2]:after").
18
+ * Uses the mutator so nested paths and mutation notations are supported.
19
+ * Returns only variables that were desired to be overridden (i.e. appear in overrides).
20
+ */
21
+ export function resolveMutationsForMultipleVariables(
22
+ variablesSchema: Record<string, VariableSchema> | undefined,
23
+ overrides: Record<string, VariableValue> | undefined,
24
+ ): Record<string, VariableValue> | undefined {
25
+ if (!overrides || Object.keys(overrides).length === 0) {
26
+ return undefined;
27
+ }
28
+ if (!variablesSchema || Object.keys(variablesSchema).length === 0) {
29
+ return undefined;
30
+ }
31
+
32
+ const variableKeysToOutput = new Set<string>();
33
+ for (const overrideKey of Object.keys(overrides)) {
34
+ const variableKey = rootVariableFromOverrideKey(overrideKey);
35
+ if (variableKey && variablesSchema[variableKey]) {
36
+ variableKeysToOutput.add(variableKey);
37
+ }
38
+ }
39
+
40
+ const result: Record<string, VariableValue> = {};
41
+
42
+ for (const variableKey of variableKeysToOutput) {
43
+ const schema = variablesSchema[variableKey];
44
+ let value: VariableValue =
45
+ schema.defaultValue !== undefined && schema.defaultValue !== null
46
+ ? (JSON.parse(JSON.stringify(schema.defaultValue)) as VariableValue)
47
+ : undefined;
48
+
49
+ const keysForThisVariable = Object.keys(overrides)
50
+ .filter(
51
+ (k) =>
52
+ rootVariableFromOverrideKey(k) === variableKey &&
53
+ (k === variableKey ||
54
+ k.startsWith(variableKey + ".") ||
55
+ k.startsWith(variableKey + "[") ||
56
+ k.startsWith(variableKey + ":")),
57
+ )
58
+ .sort((a, b) => a.length - b.length);
59
+
60
+ for (const overrideKey of keysForThisVariable) {
61
+ const overrideValue = overrides[overrideKey];
62
+ if (overrideKey === variableKey) {
63
+ value =
64
+ overrideValue !== undefined && overrideValue !== null
65
+ ? (JSON.parse(JSON.stringify(overrideValue)) as VariableValue)
66
+ : overrideValue;
67
+ } else {
68
+ const notation = overrideKey.startsWith(variableKey + "[")
69
+ ? overrideKey.slice(variableKey.length)
70
+ : overrideKey.startsWith(variableKey + ":")
71
+ ? overrideKey.slice(variableKey.length)
72
+ : overrideKey.slice(variableKey.length + 1);
73
+ value = mutate(schema, value, notation, overrideValue);
74
+ }
75
+ }
76
+
77
+ result[variableKey] = value;
78
+ }
79
+
80
+ return Object.keys(result).length > 0 ? result : undefined;
81
+ }
82
+
83
+ /**
84
+ * Resolve a single variable's override value (e.g. from variableOverrides).
85
+ * If the value is a plain object with path-like keys, it is merged with the variable's default;
86
+ * otherwise the value is returned as-is (full replacement).
87
+ */
88
+ export function resolveMutationsForSingleVariable(
89
+ variablesSchema: Record<string, VariableSchema> | undefined,
90
+ variableKey: string,
91
+ overrideValue: VariableValue,
92
+ ): VariableValue {
93
+ if (!variablesSchema || !variablesSchema[variableKey]) return overrideValue;
94
+ if (overrideValue === null || overrideValue === undefined) return overrideValue;
95
+ if (typeof overrideValue !== "object" || Array.isArray(overrideValue)) {
96
+ return overrideValue;
97
+ }
98
+ const pathMap = overrideValue as Record<string, VariableValue>;
99
+ const flat: Record<string, VariableValue> = {};
100
+ for (const [k, v] of Object.entries(pathMap)) {
101
+ flat[k === variableKey ? variableKey : variableKey + "." + k] = v;
102
+ }
103
+ const resolved = resolveMutationsForMultipleVariables(variablesSchema, flat);
104
+ return resolved && variableKey in resolved ? resolved[variableKey] : overrideValue;
105
+ }
@@ -0,0 +1,413 @@
1
+ import type { VariableSchema, VariableValue } from "@featurevisor/types";
2
+ import { mutate } from "./mutator";
3
+
4
+ const schema: VariableSchema = {
5
+ type: "object",
6
+ defaultValue: {},
7
+ };
8
+
9
+ describe("mutator", function () {
10
+ describe("mutate", function () {
11
+ test("is a function", function () {
12
+ expect(mutate).toBeInstanceOf(Function);
13
+ });
14
+
15
+ test("returns a deep clone and does not mutate original value", function () {
16
+ const value = { a: 1, b: { c: 2 } };
17
+ const result = mutate(schema, value, "a", 99);
18
+ expect(result).toEqual({ a: 99, b: { c: 2 } });
19
+ expect(value).toEqual({ a: 1, b: { c: 2 } });
20
+ });
21
+
22
+ test("returns value unchanged when notation is empty or only whitespace", function () {
23
+ const value = { a: 1 };
24
+ expect(mutate(schema, value, "", undefined)).toEqual({ a: 1 });
25
+ expect(mutate(schema, value, " ", undefined)).toEqual({ a: 1 });
26
+ });
27
+
28
+ test("returns null when value is null", function () {
29
+ expect(mutate(schema, null, "key", 1)).toBeNull();
30
+ });
31
+
32
+ test("returns undefined when value is undefined", function () {
33
+ expect(mutate(schema, undefined, "key", 1)).toBeUndefined();
34
+ });
35
+ });
36
+
37
+ describe("notation: key (set value at key)", function () {
38
+ test("sets top-level key", function () {
39
+ const value = { a: 1, b: 2 };
40
+ expect(mutate(schema, value, "a", 10)).toEqual({ a: 10, b: 2 });
41
+ expect(mutate(schema, value, "c", 3)).toEqual({ a: 1, b: 2, c: 3 });
42
+ });
43
+
44
+ test("sets key to undefined (allows clearing)", function () {
45
+ const value = { a: 1, b: 2 };
46
+ expect(mutate(schema, value, "a", undefined)).toEqual({ a: undefined, b: 2 });
47
+ });
48
+
49
+ test("sets key with string, number, boolean, object, array", function () {
50
+ const value = {};
51
+ expect(mutate(schema, value, "s", "hello")).toEqual({ s: "hello" });
52
+ expect(mutate(schema, value, "n", 42)).toEqual({ n: 42 });
53
+ expect(mutate(schema, value, "b", true)).toEqual({ b: true });
54
+ expect(mutate(schema, value, "o", { x: 1 })).toEqual({ o: { x: 1 } });
55
+ expect(mutate(schema, value, "arr", [1, 2])).toEqual({ arr: [1, 2] });
56
+ });
57
+ });
58
+
59
+ describe("notation: nested.key (set value at nested path)", function () {
60
+ test("sets nested key one level deep", function () {
61
+ const value = { a: { b: 1 } };
62
+ expect(mutate(schema, value, "a.b", 2)).toEqual({ a: { b: 2 } });
63
+ });
64
+
65
+ test("sets nested key multiple levels deep", function () {
66
+ const value = { a: { b: { c: 1 } } };
67
+ expect(mutate(schema, value, "a.b.c", 99)).toEqual({ a: { b: { c: 99 } } });
68
+ });
69
+
70
+ test("creates intermediate objects when path does not exist (no-op per current spec: returns result)", function () {
71
+ const value = { a: 1 };
72
+ const result = mutate(schema, value, "x.y.z", 10);
73
+ expect(result).toEqual({ a: 1 });
74
+ });
75
+
76
+ test("sets when intermediate path exists", function () {
77
+ const value = { level1: { level2: { level3: "old" } } };
78
+ expect(mutate(schema, value, "level1.level2.level3", "new")).toEqual({
79
+ level1: { level2: { level3: "new" } },
80
+ });
81
+ });
82
+ });
83
+
84
+ describe("notation: key:append", function () {
85
+ test("appends element to array", function () {
86
+ const value = { items: [1, 2, 3] };
87
+ expect(mutate(schema, value, "items:append", 4)).toEqual({
88
+ items: [1, 2, 3, 4],
89
+ });
90
+ });
91
+
92
+ test("appends to empty array", function () {
93
+ const value = { items: [] };
94
+ expect(mutate(schema, value, "items:append", "a")).toEqual({ items: ["a"] });
95
+ });
96
+
97
+ test("creates array when key is missing and appends", function () {
98
+ const value: VariableValue = {};
99
+ expect(mutate(schema, value, "items:append", 1)).toEqual({ items: [1] });
100
+ });
101
+
102
+ test("root-level array: notation :append appends to array value", function () {
103
+ const arraySchema: VariableSchema = { type: "array", defaultValue: [] };
104
+ expect(mutate(arraySchema, ["a", "b"], ":append", "c")).toEqual(["a", "b", "c"]);
105
+ });
106
+
107
+ test("appends object to array", function () {
108
+ const value = { list: [{ id: 1 }] };
109
+ expect(mutate(schema, value, "list:append", { id: 2, name: "b" })).toEqual({
110
+ list: [{ id: 1 }, { id: 2, name: "b" }],
111
+ });
112
+ });
113
+ });
114
+
115
+ describe("notation: key:prepend", function () {
116
+ test("prepends element to array", function () {
117
+ const value = { items: [1, 2, 3] };
118
+ expect(mutate(schema, value, "items:prepend", 0)).toEqual({
119
+ items: [0, 1, 2, 3],
120
+ });
121
+ });
122
+
123
+ test("prepends to empty array", function () {
124
+ const value = { items: [] };
125
+ expect(mutate(schema, value, "items:prepend", "first")).toEqual({
126
+ items: ["first"],
127
+ });
128
+ });
129
+
130
+ test("creates array when key is missing and prepends", function () {
131
+ const value: VariableValue = {};
132
+ expect(mutate(schema, value, "items:prepend", "x")).toEqual({ items: ["x"] });
133
+ });
134
+
135
+ test("root-level array: notation :prepend prepends to array value", function () {
136
+ const arraySchema: VariableSchema = { type: "array", defaultValue: [] };
137
+ expect(mutate(arraySchema, ["b", "c"], ":prepend", "a")).toEqual(["a", "b", "c"]);
138
+ });
139
+ });
140
+
141
+ describe("notation: key[id=123]:after", function () {
142
+ test("inserts element after matching item by selector", function () {
143
+ const value = {
144
+ items: [
145
+ { id: "1", name: "a" },
146
+ { id: "123", name: "b" },
147
+ { id: "3", name: "c" },
148
+ ],
149
+ };
150
+ const result = mutate(schema, value, "items[id=123]:after", {
151
+ id: "2",
152
+ name: "inserted",
153
+ });
154
+ expect(result).toEqual({
155
+ items: [
156
+ { id: "1", name: "a" },
157
+ { id: "123", name: "b" },
158
+ { id: "2", name: "inserted" },
159
+ { id: "3", name: "c" },
160
+ ],
161
+ });
162
+ });
163
+
164
+ test("inserts after when id is numeric in data", function () {
165
+ const value = {
166
+ list: [
167
+ { id: 123, name: "a" },
168
+ { id: 456, name: "b" },
169
+ ],
170
+ };
171
+ const result = mutate(schema, value, "list[id=123]:after", {
172
+ id: 124,
173
+ name: "after 123",
174
+ });
175
+ expect(result).toEqual({
176
+ list: [
177
+ { id: 123, name: "a" },
178
+ { id: 124, name: "after 123" },
179
+ { id: 456, name: "b" },
180
+ ],
181
+ });
182
+ });
183
+
184
+ test("no-op when no element matches selector", function () {
185
+ const value = { items: [{ id: "1" }] };
186
+ const result = mutate(schema, value, "items[id=999]:after", { id: "new" });
187
+ expect(result).toEqual({ items: [{ id: "1" }] });
188
+ });
189
+ });
190
+
191
+ describe("notation: key[id=123]:before", function () {
192
+ test("inserts element before matching item by selector", function () {
193
+ const value = {
194
+ items: [
195
+ { id: "1", name: "a" },
196
+ { id: "123", name: "b" },
197
+ { id: "3", name: "c" },
198
+ ],
199
+ };
200
+ const result = mutate(schema, value, "items[id=123]:before", {
201
+ id: "0",
202
+ name: "inserted",
203
+ });
204
+ expect(result).toEqual({
205
+ items: [
206
+ { id: "1", name: "a" },
207
+ { id: "0", name: "inserted" },
208
+ { id: "123", name: "b" },
209
+ { id: "3", name: "c" },
210
+ ],
211
+ });
212
+ });
213
+
214
+ test("inserts before first element when first matches", function () {
215
+ const value = { list: [{ id: 1 }, { id: 2 }] };
216
+ const result = mutate(schema, value, "list[id=1]:before", { id: 0 });
217
+ expect(result).toEqual({
218
+ list: [{ id: 0 }, { id: 1 }, { id: 2 }],
219
+ });
220
+ });
221
+ });
222
+
223
+ describe("notation: key[n] (set value at index)", function () {
224
+ test("sets element at array index", function () {
225
+ const value = { items: [10, 20, 30] };
226
+ expect(mutate(schema, value, "items[0]", 1)).toEqual({
227
+ items: [1, 20, 30],
228
+ });
229
+ expect(mutate(schema, value, "items[1]", 200)).toEqual({
230
+ items: [10, 200, 30],
231
+ });
232
+ expect(mutate(schema, value, "items[2]", 300)).toEqual({
233
+ items: [10, 20, 300],
234
+ });
235
+ });
236
+
237
+ test("sets object at index", function () {
238
+ const value = { list: [{ id: 1 }, { id: 2 }] };
239
+ expect(mutate(schema, value, "list[0]", { id: 10 })).toEqual({
240
+ list: [{ id: 10 }, { id: 2 }],
241
+ });
242
+ });
243
+
244
+ test("root-level array: notation [n] sets element at index", function () {
245
+ const arraySchema: VariableSchema = { type: "array", defaultValue: [] };
246
+ const value = [10, 20, 30];
247
+ expect(mutate(arraySchema, value, "[0]", 1)).toEqual([1, 20, 30]);
248
+ expect(mutate(arraySchema, value, "[1]", 99)).toEqual([10, 99, 30]);
249
+ expect(mutate(arraySchema, [100], "[0]", 200)).toEqual([200]);
250
+ });
251
+ });
252
+
253
+ describe("notation: key[n].key (set property of item at index)", function () {
254
+ test("sets property of element at index", function () {
255
+ const value = {
256
+ items: [
257
+ { id: 1, name: "a" },
258
+ { id: 2, name: "b" },
259
+ ],
260
+ };
261
+ expect(mutate(schema, value, "items[0].name", "A")).toEqual({
262
+ items: [
263
+ { id: 1, name: "A" },
264
+ { id: 2, name: "b" },
265
+ ],
266
+ });
267
+ expect(mutate(schema, value, "items[1].id", 20)).toEqual({
268
+ items: [
269
+ { id: 1, name: "a" },
270
+ { id: 20, name: "b" },
271
+ ],
272
+ });
273
+ });
274
+
275
+ test("sets nested property of element at index", function () {
276
+ const value = {
277
+ list: [{ meta: { count: 1 } }],
278
+ };
279
+ expect(mutate(schema, value, "list[0].meta.count", 5)).toEqual({
280
+ list: [{ meta: { count: 5 } }],
281
+ });
282
+ });
283
+ });
284
+
285
+ describe("notation: key[n]:remove", function () {
286
+ test("removes element at index", function () {
287
+ const value = { items: [1, 2, 3] };
288
+ expect(mutate(schema, value, "items[1]:remove", undefined)).toEqual({
289
+ items: [1, 3],
290
+ });
291
+ });
292
+
293
+ test("removes first element", function () {
294
+ const value = { items: ["a", "b", "c"] };
295
+ expect(mutate(schema, value, "items[0]:remove", undefined)).toEqual({
296
+ items: ["b", "c"],
297
+ });
298
+ });
299
+
300
+ test("removes last element", function () {
301
+ const value = { items: ["a", "b", "c"] };
302
+ expect(mutate(schema, value, "items[2]:remove", undefined)).toEqual({
303
+ items: ["a", "b"],
304
+ });
305
+ });
306
+
307
+ test("removes only element", function () {
308
+ const value = { items: [1] };
309
+ expect(mutate(schema, value, "items[0]:remove", undefined)).toEqual({
310
+ items: [],
311
+ });
312
+ });
313
+ });
314
+
315
+ describe("notation: key[id=123]:remove", function () {
316
+ test("removes element matching selector", function () {
317
+ const value = {
318
+ items: [
319
+ { id: "1", name: "a" },
320
+ { id: "123", name: "b" },
321
+ { id: "3", name: "c" },
322
+ ],
323
+ };
324
+ expect(mutate(schema, value, "items[id=123]:remove", undefined)).toEqual({
325
+ items: [
326
+ { id: "1", name: "a" },
327
+ { id: "3", name: "c" },
328
+ ],
329
+ });
330
+ });
331
+
332
+ test("removes by numeric id", function () {
333
+ const value = { list: [{ id: 123 }, { id: 456 }] };
334
+ expect(mutate(schema, value, "list[id=123]:remove", undefined)).toEqual({
335
+ list: [{ id: 456 }],
336
+ });
337
+ });
338
+
339
+ test("no-op when no element matches", function () {
340
+ const value = { items: [{ id: "1" }] };
341
+ expect(mutate(schema, value, "items[id=999]:remove", undefined)).toEqual({
342
+ items: [{ id: "1" }],
343
+ });
344
+ });
345
+ });
346
+
347
+ describe("notation: key:remove", function () {
348
+ test("removes top-level property", function () {
349
+ const value = { a: 1, b: 2, c: 3 };
350
+ expect(mutate(schema, value, "b:remove", undefined)).toEqual({
351
+ a: 1,
352
+ c: 3,
353
+ });
354
+ });
355
+
356
+ test("removes nested property", function () {
357
+ const value = { a: { b: 1, c: 2 } };
358
+ expect(mutate(schema, value, "a.c:remove", undefined)).toEqual({
359
+ a: { b: 1 },
360
+ });
361
+ });
362
+
363
+ test("removes when value is undefined (key no longer present)", function () {
364
+ const value = { x: "present" };
365
+ const result = mutate(schema, value, "x:remove", undefined);
366
+ expect(result).toEqual({});
367
+ expect(Object.prototype.hasOwnProperty.call(result, "x")).toBe(false);
368
+ });
369
+ });
370
+
371
+ describe("edge cases and combined paths", function () {
372
+ test("key with brackets in selector (prop=value)", function () {
373
+ const value = { list: [{ id: "a=1", name: "first" }] };
374
+ const result = mutate(schema, value, "list[id=a=1].name", "updated");
375
+ expect(result).toEqual({ list: [{ id: "a=1", name: "updated" }] });
376
+ });
377
+
378
+ test("array of primitives: set and remove by index", function () {
379
+ const value = { nums: [1, 2, 3] };
380
+ expect(mutate(schema, value, "nums[1]", 20)).toEqual({ nums: [1, 20, 3] });
381
+ expect(mutate(schema, value, "nums[2]:remove", undefined)).toEqual({
382
+ nums: [1, 2],
383
+ });
384
+ });
385
+
386
+ test("deep path: a.b.c.d", function () {
387
+ const value = { a: { b: { c: { d: "old" } } } };
388
+ expect(mutate(schema, value, "a.b.c.d", "new")).toEqual({
389
+ a: { b: { c: { d: "new" } } },
390
+ });
391
+ });
392
+
393
+ test("selector with different property name", function () {
394
+ const value = {
395
+ users: [
396
+ { userId: "u1", name: "Alice" },
397
+ { userId: "u2", name: "Bob" },
398
+ ],
399
+ };
400
+ expect(mutate(schema, value, "users[userId=u2]:remove", undefined)).toEqual({
401
+ users: [{ userId: "u1", name: "Alice" }],
402
+ });
403
+ });
404
+
405
+ test("append and prepend in sequence (each from fresh clone)", function () {
406
+ const value = { arr: [2] };
407
+ const afterAppend = mutate(schema, value, "arr:append", 3);
408
+ expect(afterAppend).toEqual({ arr: [2, 3] });
409
+ const afterPrepend = mutate(schema, value, "arr:prepend", 1);
410
+ expect(afterPrepend).toEqual({ arr: [1, 2] });
411
+ });
412
+ });
413
+ });