@voidhash/mimic 1.0.0-beta.15 → 1.0.0-beta.17

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 (112) hide show
  1. package/dist/Document.cjs +0 -3
  2. package/dist/Document.d.mts.map +1 -1
  3. package/dist/Document.mjs +0 -3
  4. package/dist/Document.mjs.map +1 -1
  5. package/dist/EffectSchema.cjs +3 -3
  6. package/dist/EffectSchema.d.cts +5 -5
  7. package/dist/EffectSchema.d.cts.map +1 -1
  8. package/dist/EffectSchema.d.mts +5 -5
  9. package/dist/EffectSchema.d.mts.map +1 -1
  10. package/dist/EffectSchema.mjs +3 -3
  11. package/dist/EffectSchema.mjs.map +1 -1
  12. package/dist/FractionalIndex.mjs.map +1 -1
  13. package/dist/Operation.d.cts +4 -4
  14. package/dist/Operation.d.cts.map +1 -1
  15. package/dist/Operation.d.mts +4 -4
  16. package/dist/Operation.d.mts.map +1 -1
  17. package/dist/Operation.mjs.map +1 -1
  18. package/dist/OperationDefinition.d.cts +2 -2
  19. package/dist/OperationDefinition.d.cts.map +1 -1
  20. package/dist/OperationDefinition.d.mts +2 -2
  21. package/dist/OperationDefinition.d.mts.map +1 -1
  22. package/dist/OperationDefinition.mjs.map +1 -1
  23. package/dist/Presence.mjs.map +1 -1
  24. package/dist/Primitive.d.cts +2 -2
  25. package/dist/Primitive.d.mts +2 -2
  26. package/dist/SchemaJSON.cjs +305 -0
  27. package/dist/SchemaJSON.d.cts +11 -0
  28. package/dist/SchemaJSON.d.cts.map +1 -0
  29. package/dist/SchemaJSON.d.mts +11 -0
  30. package/dist/SchemaJSON.d.mts.map +1 -0
  31. package/dist/SchemaJSON.mjs +301 -0
  32. package/dist/SchemaJSON.mjs.map +1 -0
  33. package/dist/index.cjs +7 -0
  34. package/dist/index.d.cts +2 -1
  35. package/dist/index.d.mts +2 -1
  36. package/dist/index.mjs +2 -1
  37. package/dist/primitives/Array.cjs +12 -2
  38. package/dist/primitives/Array.d.cts.map +1 -1
  39. package/dist/primitives/Array.d.mts.map +1 -1
  40. package/dist/primitives/Array.mjs +12 -2
  41. package/dist/primitives/Array.mjs.map +1 -1
  42. package/dist/primitives/Boolean.mjs.map +1 -1
  43. package/dist/primitives/Either.mjs.map +1 -1
  44. package/dist/primitives/Literal.mjs.map +1 -1
  45. package/dist/primitives/Number.cjs +27 -5
  46. package/dist/primitives/Number.d.cts.map +1 -1
  47. package/dist/primitives/Number.d.mts.map +1 -1
  48. package/dist/primitives/Number.mjs +27 -5
  49. package/dist/primitives/Number.mjs.map +1 -1
  50. package/dist/primitives/String.cjs +44 -13
  51. package/dist/primitives/String.d.cts.map +1 -1
  52. package/dist/primitives/String.d.mts.map +1 -1
  53. package/dist/primitives/String.mjs +44 -13
  54. package/dist/primitives/String.mjs.map +1 -1
  55. package/dist/primitives/Struct.cjs +48 -9
  56. package/dist/primitives/Struct.d.cts +22 -3
  57. package/dist/primitives/Struct.d.cts.map +1 -1
  58. package/dist/primitives/Struct.d.mts +22 -3
  59. package/dist/primitives/Struct.d.mts.map +1 -1
  60. package/dist/primitives/Struct.mjs +48 -9
  61. package/dist/primitives/Struct.mjs.map +1 -1
  62. package/dist/primitives/Union.mjs.map +1 -1
  63. package/dist/primitives/shared.cjs +2 -5
  64. package/dist/primitives/shared.d.cts +2 -4
  65. package/dist/primitives/shared.d.cts.map +1 -1
  66. package/dist/primitives/shared.d.mts +2 -4
  67. package/dist/primitives/shared.d.mts.map +1 -1
  68. package/dist/primitives/shared.mjs +2 -5
  69. package/dist/primitives/shared.mjs.map +1 -1
  70. package/package.json +15 -8
  71. package/src/Document.ts +13 -4
  72. package/src/EffectSchema.ts +3 -3
  73. package/src/FractionalIndex.ts +18 -18
  74. package/src/Operation.ts +5 -5
  75. package/src/OperationDefinition.ts +2 -2
  76. package/src/Presence.ts +3 -3
  77. package/src/SchemaJSON.ts +396 -0
  78. package/src/index.ts +1 -0
  79. package/src/primitives/Array.ts +18 -8
  80. package/src/primitives/Boolean.ts +2 -2
  81. package/src/primitives/Either.ts +2 -2
  82. package/src/primitives/Literal.ts +2 -2
  83. package/src/primitives/Number.ts +44 -22
  84. package/src/primitives/String.ts +61 -34
  85. package/src/primitives/Struct.ts +100 -12
  86. package/src/primitives/Union.ts +1 -1
  87. package/src/primitives/shared.ts +12 -2
  88. package/.turbo/turbo-build.log +0 -270
  89. package/tests/Document.test.ts +0 -557
  90. package/tests/EffectSchema.test.ts +0 -546
  91. package/tests/FractionalIndex.test.ts +0 -377
  92. package/tests/OperationPath.test.ts +0 -151
  93. package/tests/Presence.test.ts +0 -321
  94. package/tests/Primitive.test.ts +0 -381
  95. package/tests/client/ClientDocument.test.ts +0 -1981
  96. package/tests/client/WebSocketTransport.test.ts +0 -1217
  97. package/tests/primitives/Array.test.ts +0 -526
  98. package/tests/primitives/Boolean.test.ts +0 -126
  99. package/tests/primitives/Either.test.ts +0 -707
  100. package/tests/primitives/Lazy.test.ts +0 -143
  101. package/tests/primitives/Literal.test.ts +0 -122
  102. package/tests/primitives/Number.test.ts +0 -133
  103. package/tests/primitives/String.test.ts +0 -128
  104. package/tests/primitives/Struct.test.ts +0 -1044
  105. package/tests/primitives/Tree.test.ts +0 -1139
  106. package/tests/primitives/TreeNode.test.ts +0 -50
  107. package/tests/primitives/Union.test.ts +0 -554
  108. package/tests/server/ServerDocument.test.ts +0 -903
  109. package/tsconfig.build.json +0 -24
  110. package/tsconfig.json +0 -8
  111. package/tsdown.config.ts +0 -18
  112. package/vitest.mts +0 -11
@@ -1,1139 +0,0 @@
1
- import { describe, expect, it } from "@effect/vitest";
2
- import * as Primitive from "../../src/Primitive";
3
- import * as ProxyEnvironment from "../../src/ProxyEnvironment";
4
- import * as OperationPath from "../../src/OperationPath";
5
- import * as Operation from "../../src/Operation";
6
-
7
- const hasOwn = (value: unknown, key: string): boolean =>
8
- Object.prototype.hasOwnProperty.call(value, key);
9
-
10
- describe("TreePrimitive", () => {
11
- // Define node types using the new TreeNode API
12
- const FileNode = Primitive.TreeNode("file", {
13
- data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number() }),
14
- children: [] as const,
15
- });
16
-
17
- const FolderNode = Primitive.TreeNode("folder", {
18
- data: Primitive.Struct({ name: Primitive.String() }),
19
- children: (): readonly Primitive.AnyTreeNodePrimitive[] => [FolderNode, FileNode],
20
- });
21
-
22
- const fileSystemTree = Primitive.Tree({
23
- root: FolderNode,
24
- });
25
-
26
- // Helper to create a mock environment with state access
27
- const createEnvWithState = (
28
- state: Primitive.TreeState<typeof FolderNode> = []
29
- ): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
30
- const operations: Operation.Operation<any, any, any>[] = [];
31
- let currentState = [...state] as Primitive.TreeState<typeof FolderNode>;
32
- let idCounter = 0;
33
-
34
- const env = ProxyEnvironment.make({
35
- onOperation: (op) => {
36
- operations.push(op);
37
- // Apply operation to keep state in sync
38
- currentState = fileSystemTree._internal.applyOperation(currentState, op);
39
- },
40
- getState: () => currentState,
41
- generateId: () => `node-${++idCounter}`,
42
- });
43
-
44
- return { env, operations };
45
- };
46
-
47
- describe("schema", () => {
48
- it("exposes root node type", () => {
49
- expect(fileSystemTree.root).toBe(FolderNode);
50
- expect(fileSystemTree.root.type).toBe("folder");
51
- });
52
-
53
- it("required() returns a new TreePrimitive", () => {
54
- const required = fileSystemTree.required();
55
- expect(required).toBeInstanceOf(Primitive.TreePrimitive);
56
- expect(required).not.toBe(fileSystemTree);
57
- });
58
-
59
- it("default() returns a new TreePrimitive with default value", () => {
60
- const defaultInput = {
61
- type: "folder" as const,
62
- id: "root",
63
- name: "Root",
64
- children: [],
65
- };
66
- const withDefault = fileSystemTree.default(defaultInput);
67
- const initialState = withDefault._internal.getInitialState();
68
- expect(initialState).toHaveLength(1);
69
- expect(initialState![0]!.id).toBe("root");
70
- expect(initialState![0]!.type).toBe("folder");
71
- expect(initialState![0]!.data).toEqual({ name: "Root" });
72
- });
73
- });
74
-
75
- describe("proxy - basic operations", () => {
76
- it("get() returns empty array for initial state", () => {
77
- const { env } = createEnvWithState();
78
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
79
-
80
- expect(proxy.get()).toEqual([]);
81
- });
82
-
83
- it("set() generates tree.set operation with nested input converted to flat", () => {
84
- const { env, operations } = createEnvWithState();
85
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
86
-
87
- const nestedInput = {
88
- type: "folder" as const,
89
- name: "Root",
90
- children: [],
91
- };
92
- proxy.set(nestedInput);
93
-
94
- expect(operations).toHaveLength(1);
95
- expect(operations[0]!.kind).toBe("tree.set");
96
- // The payload should be flat format
97
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
98
- expect(payload).toHaveLength(1);
99
- expect(payload[0]!.type).toBe("folder");
100
- expect(payload[0]!.data).toEqual({ name: "Root" });
101
- expect(payload[0]!.parentId).toBe(null);
102
- expect(payload[0]!.id).toBe("node-1"); // Generated by env.generateId
103
- });
104
-
105
- it("root() returns the root node", () => {
106
- const initialState: Primitive.TreeState<typeof FolderNode> = [
107
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
108
- { id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
109
- ];
110
- const { env } = createEnvWithState(initialState);
111
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
112
-
113
- const root = proxy.root();
114
- expect(root).toBeDefined();
115
- expect(root!.id).toBe("root");
116
- expect(root!.parentId).toBe(null);
117
- });
118
-
119
- it("node() returns a node proxy by ID", () => {
120
- const initialState: Primitive.TreeState<typeof FolderNode> = [
121
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
122
- { id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
123
- ];
124
- const { env } = createEnvWithState(initialState);
125
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
126
-
127
- const node = proxy.node("child1");
128
- expect(node).toBeDefined();
129
- expect(node!.id).toBe("child1");
130
- expect(node!.type).toBe("file");
131
- expect(node!.get().data).toEqual({ name: "File1", size: 100 });
132
- });
133
-
134
- it("children() returns ordered children", () => {
135
- const initialState: Primitive.TreeState<typeof FolderNode> = [
136
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
137
- { id: "child2", type: "file", parentId: "root", pos: "a1", data: { name: "File2", size: 200 } },
138
- { id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
139
- ];
140
- const { env } = createEnvWithState(initialState);
141
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
142
-
143
- const children = proxy.children("root");
144
- expect(children).toHaveLength(2);
145
- expect(children[0]!.id).toBe("child1"); // a0 comes first
146
- expect(children[1]!.id).toBe("child2"); // a1 comes second
147
- });
148
- });
149
-
150
- describe("proxy - type narrowing with is() and as()", () => {
151
- it("is() returns true for matching node type", () => {
152
- const initialState: Primitive.TreeState<typeof FolderNode> = [
153
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
154
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
155
- ];
156
- const { env } = createEnvWithState(initialState);
157
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
158
-
159
- const fileProxy = proxy.node("file1");
160
- expect(fileProxy!.is(FileNode)).toBe(true);
161
- expect(fileProxy!.is(FolderNode)).toBe(false);
162
-
163
- const folderProxy = proxy.node("root");
164
- expect(folderProxy!.is(FolderNode)).toBe(true);
165
- expect(folderProxy!.is(FileNode)).toBe(false);
166
- });
167
-
168
- it("as() returns typed proxy for correct type", () => {
169
- const initialState: Primitive.TreeState<typeof FolderNode> = [
170
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
171
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
172
- ];
173
- const { env, operations } = createEnvWithState(initialState);
174
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
175
-
176
- const fileProxy = proxy.node("file1")!.as(FileNode);
177
- expect(fileProxy.id).toBe("file1");
178
- expect(fileProxy.type).toBe("file");
179
-
180
- // Type-safe data access
181
- fileProxy.data.name.set("UpdatedName");
182
- expect(operations).toHaveLength(1);
183
- expect(operations[0]!.kind).toBe("string.set");
184
- });
185
-
186
- it("as() throws for wrong type", () => {
187
- const initialState: Primitive.TreeState<typeof FolderNode> = [
188
- { id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
189
- ];
190
- const { env } = createEnvWithState(initialState);
191
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
192
-
193
- const nodeProxy = proxy.node("file1");
194
- expect(() => nodeProxy!.as(FolderNode)).toThrow(Primitive.ValidationError);
195
- });
196
- });
197
-
198
- describe("proxy - insert operations with TreeNode types", () => {
199
- it("insertFirst() creates node at beginning of children", () => {
200
- const initialState: Primitive.TreeState<typeof FolderNode> = [
201
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
202
- { id: "existing", type: "file", parentId: "root", pos: "a1", data: { name: "Existing", size: 100 } },
203
- ];
204
- const { env, operations } = createEnvWithState(initialState);
205
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
206
-
207
- const newId = proxy.insertFirst("root", FileNode, { name: "First", size: 50 });
208
-
209
- expect(operations).toHaveLength(1);
210
- expect(operations[0]!.kind).toBe("tree.insert");
211
- expect(newId).toBe("node-1");
212
-
213
- const payload = operations[0]!.payload as { id: string; pos: string; type: string };
214
- expect(payload.type).toBe("file");
215
- expect(payload.pos < "a1").toBe(true); // Should be before existing
216
- });
217
-
218
- it("insertLast() creates node at end of children", () => {
219
- const initialState: Primitive.TreeState<typeof FolderNode> = [
220
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
221
- { id: "existing", type: "file", parentId: "root", pos: "a0", data: { name: "Existing", size: 100 } },
222
- ];
223
- const { env, operations } = createEnvWithState(initialState);
224
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
225
-
226
- proxy.insertLast("root", FileNode, { name: "Last", size: 50 });
227
-
228
- const payload = operations[0]!.payload as { pos: string };
229
- expect(payload.pos > "a0").toBe(true); // Should be after existing
230
- });
231
-
232
- it("insertFirst() with null parentId creates root node", () => {
233
- const { env, operations } = createEnvWithState();
234
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
235
-
236
- proxy.insertFirst(null, FolderNode, { name: "Root" });
237
-
238
- expect(operations).toHaveLength(1);
239
- const payload = operations[0]!.payload as { parentId: string | null; type: string };
240
- expect(payload.parentId).toBe(null);
241
- expect(payload.type).toBe("folder");
242
- });
243
- });
244
-
245
- describe("proxy - validation", () => {
246
- it("throws when inserting invalid child type", () => {
247
- const initialState: Primitive.TreeState<typeof FolderNode> = [
248
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
249
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
250
- ];
251
- const { env } = createEnvWithState(initialState);
252
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
253
-
254
- // Files cannot have children
255
- expect(() => proxy.insertFirst("file1", FileNode, { name: "Child", size: 50 })).toThrow(
256
- Primitive.ValidationError
257
- );
258
- });
259
-
260
- it("throws when inserting non-root type at root level", () => {
261
- const { env } = createEnvWithState();
262
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
263
-
264
- expect(() => proxy.insertFirst(null, FileNode, { name: "File", size: 50 })).toThrow(
265
- Primitive.ValidationError
266
- );
267
- });
268
-
269
- it("throws when inserting second root", () => {
270
- const initialState: Primitive.TreeState<typeof FolderNode> = [
271
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
272
- ];
273
- const { env } = createEnvWithState(initialState);
274
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
275
-
276
- expect(() => proxy.insertFirst(null, FolderNode, { name: "SecondRoot" })).toThrow(
277
- Primitive.ValidationError
278
- );
279
- });
280
- });
281
-
282
- describe("proxy - toSnapshot()", () => {
283
- it("returns undefined for empty tree", () => {
284
- const { env } = createEnvWithState();
285
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
286
-
287
- expect(proxy.toSnapshot()).toBeUndefined();
288
- });
289
-
290
- it("returns nested snapshot with spread data", () => {
291
- const initialState: Primitive.TreeState<typeof FolderNode> = [
292
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
293
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
294
- { id: "folder1", type: "folder", parentId: "root", pos: "a1", data: { name: "Subfolder" } },
295
- { id: "file2", type: "file", parentId: "folder1", pos: "a0", data: { name: "File2", size: 200 } },
296
- ];
297
- const { env } = createEnvWithState(initialState);
298
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
299
-
300
-
301
- const snapshot = proxy.toSnapshot();
302
- expect(snapshot).toBeDefined();
303
- expect(snapshot!.id).toBe("root");
304
- expect(snapshot!.type).toBe("folder");
305
- expect(snapshot!.parentId).toBe(null);
306
- expect(snapshot!.pos).toBe("a0");
307
- expect(snapshot!.name).toBe("Root"); // Data spread at node level
308
- expect(snapshot!.children).toHaveLength(2);
309
-
310
- const file1Snapshot = snapshot!.children[0]!;
311
- expect(file1Snapshot.id).toBe("file1");
312
- expect(file1Snapshot.parentId).toBe("root");
313
- expect(file1Snapshot.pos).toBe("a0");
314
- expect(file1Snapshot.name).toBe("File1");
315
- expect(file1Snapshot.children).toEqual([]);
316
-
317
- const folder1Snapshot = snapshot!.children[1]!;
318
- expect(folder1Snapshot.id).toBe("folder1");
319
- expect(folder1Snapshot.children).toHaveLength(1);
320
- expect(folder1Snapshot.children[0]!.name).toBe("File2");
321
- });
322
- });
323
-
324
- describe("proxy - at() with typed node", () => {
325
- it("at() returns typed proxy for node data", () => {
326
- const initialState: Primitive.TreeState<typeof FolderNode> = [
327
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
328
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
329
- ];
330
- const { env, operations } = createEnvWithState(initialState);
331
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
332
-
333
- proxy.at("file1", FileNode).name.set("UpdatedName");
334
-
335
- expect(operations).toHaveLength(1);
336
- expect(operations[0]!.kind).toBe("string.set");
337
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
338
- expect(operations[0]!.payload).toBe("UpdatedName");
339
- });
340
-
341
- it("at() throws when node type mismatch", () => {
342
- const initialState: Primitive.TreeState<typeof FolderNode> = [
343
- { id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
344
- ];
345
- const { env } = createEnvWithState(initialState);
346
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
347
-
348
- expect(() => proxy.at("file1", FolderNode)).toThrow(Primitive.ValidationError);
349
- });
350
- });
351
-
352
- describe("proxy - move operations", () => {
353
- it("move() changes parent and position", () => {
354
- const initialState: Primitive.TreeState<typeof FolderNode> = [
355
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
356
- { id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
357
- { id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
358
- { id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
359
- ];
360
- const { env, operations } = createEnvWithState(initialState);
361
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
362
-
363
- // Move file1 to folder2
364
- proxy.move("file1", "folder2", 0);
365
-
366
- expect(operations).toHaveLength(1);
367
- expect(operations[0]!.kind).toBe("tree.move");
368
- const payload = operations[0]!.payload as { id: string; parentId: string };
369
- expect(payload.id).toBe("file1");
370
- expect(payload.parentId).toBe("folder2");
371
- });
372
-
373
- it("throws when moving node to its descendant (cycle prevention)", () => {
374
- const initialState: Primitive.TreeState<typeof FolderNode> = [
375
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
376
- { id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
377
- { id: "folder2", type: "folder", parentId: "folder1", pos: "a0", data: { name: "Folder2" } },
378
- ];
379
- const { env } = createEnvWithState(initialState);
380
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
381
-
382
- expect(() => proxy.move("folder1", "folder2", 0)).toThrow(Primitive.ValidationError);
383
- });
384
- });
385
-
386
- describe("applyOperation", () => {
387
- it("tree.set replaces entire tree", () => {
388
- const newNodes: Primitive.TreeState<typeof FolderNode> = [
389
- { id: "new-root", type: "folder", parentId: null, pos: "a0", data: { name: "NewRoot" } },
390
- ];
391
- const operation: Operation.Operation<any, any, any> = {
392
- kind: "tree.set",
393
- path: OperationPath.make(""),
394
- payload: newNodes,
395
- };
396
-
397
- const result = fileSystemTree._internal.applyOperation([], operation);
398
- expect(result).toEqual(newNodes);
399
- });
400
-
401
- it("tree.insert adds a new node", () => {
402
- const initialState: Primitive.TreeState<typeof FolderNode> = [
403
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
404
- ];
405
- const operation: Operation.Operation<any, any, any> = {
406
- kind: "tree.insert",
407
- path: OperationPath.make(""),
408
- payload: {
409
- id: "file1",
410
- type: "file",
411
- parentId: "root",
412
- pos: "a0",
413
- data: { name: "File1", size: 100 },
414
- },
415
- };
416
-
417
- const result = fileSystemTree._internal.applyOperation(initialState, operation);
418
- expect(result).toHaveLength(2);
419
- expect(result[1]).toEqual({
420
- id: "file1",
421
- type: "file",
422
- parentId: "root",
423
- pos: "a0",
424
- data: { name: "File1", size: 100 },
425
- });
426
- });
427
-
428
- it("tree.remove removes node and descendants", () => {
429
- const initialState: Primitive.TreeState<typeof FolderNode> = [
430
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
431
- { id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
432
- { id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
433
- { id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
434
- ];
435
- const operation: Operation.Operation<any, any, any> = {
436
- kind: "tree.remove",
437
- path: OperationPath.make(""),
438
- payload: { id: "folder1" },
439
- };
440
-
441
- const result = fileSystemTree._internal.applyOperation(initialState, operation);
442
- expect(result).toHaveLength(2);
443
- expect(result.map(n => n.id)).toEqual(["root", "folder2"]);
444
- });
445
-
446
- it("delegates node data operations", () => {
447
- const initialState: Primitive.TreeState<typeof FolderNode> = [
448
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
449
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
450
- ];
451
- const operation: Operation.Operation<any, any, any> = {
452
- kind: "string.set",
453
- path: OperationPath.make("file1/name"),
454
- payload: "UpdatedName",
455
- };
456
-
457
- const result = fileSystemTree._internal.applyOperation(initialState, operation);
458
- const updatedNode = result.find(n => n.id === "file1");
459
- expect(updatedNode!.data).toEqual({ name: "UpdatedName", size: 100 });
460
- });
461
- });
462
-
463
- describe("getInitialState", () => {
464
- it("automatically creates a root node when no default is set", () => {
465
- const initialState = fileSystemTree._internal.getInitialState();
466
- expect(initialState).toBeDefined();
467
- expect(initialState).toHaveLength(1);
468
- expect(initialState![0]).toMatchObject({
469
- type: "folder",
470
- parentId: null,
471
- data: {},
472
- });
473
- // Verify ID and pos are generated
474
- expect(typeof initialState![0]!.id).toBe("string");
475
- expect(typeof initialState![0]!.pos).toBe("string");
476
- });
477
-
478
- it("returns the default value when set (converted from nested)", () => {
479
- const defaultInput = {
480
- type: "folder" as const,
481
- id: "root",
482
- name: "Root",
483
- children: [],
484
- };
485
- const withDefault = fileSystemTree.default(defaultInput);
486
- const initialState = withDefault._internal.getInitialState();
487
- expect(initialState).toHaveLength(1);
488
- expect(initialState![0]!.id).toBe("root");
489
- expect(initialState![0]!.type).toBe("folder");
490
- expect(initialState![0]!.parentId).toBe(null);
491
- expect(initialState![0]!.data).toEqual({ name: "Root" });
492
- });
493
- });
494
-
495
- describe("proxy - partial update", () => {
496
- it("update() on TypedNodeProxy updates only specified fields", () => {
497
- const initialState: Primitive.TreeState<typeof FolderNode> = [
498
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
499
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
500
- ];
501
- const { env, operations } = createEnvWithState(initialState);
502
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
503
-
504
- // Use the update method via as()
505
- const fileProxy = proxy.node("file1")!.as(FileNode);
506
- fileProxy.update({ name: "UpdatedName" });
507
-
508
- // Should generate only a string.set operation for the name field
509
- expect(operations).toHaveLength(1);
510
- expect(operations[0]!.kind).toBe("string.set");
511
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
512
- expect(operations[0]!.payload).toBe("UpdatedName");
513
- });
514
-
515
- it("update() preserves other fields when updating partial data", () => {
516
- const initialState: Primitive.TreeState<typeof FolderNode> = [
517
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
518
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
519
- ];
520
- const { env, operations } = createEnvWithState(initialState);
521
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
522
-
523
- // Update only the size field
524
- proxy.node("file1")!.as(FileNode).update({ size: 200 });
525
-
526
- // Should generate only a number.set operation for the size field
527
- expect(operations).toHaveLength(1);
528
- expect(operations[0]!.kind).toBe("number.set");
529
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
530
- expect(operations[0]!.payload).toBe(200);
531
-
532
- // The name should remain unchanged in the state
533
- const updatedState = proxy.get();
534
- const file1 = updatedState.find(n => n.id === "file1");
535
- expect(file1!.data).toEqual({ name: "File1", size: 200 });
536
- });
537
-
538
- it("update() handles multiple fields at once", () => {
539
- const initialState: Primitive.TreeState<typeof FolderNode> = [
540
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
541
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
542
- ];
543
- const { env, operations } = createEnvWithState(initialState);
544
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
545
-
546
- // Update both name and size
547
- proxy.node("file1")!.as(FileNode).update({ name: "NewFile", size: 500 });
548
-
549
- // Should generate two operations
550
- expect(operations).toHaveLength(2);
551
-
552
- // Verify both fields were updated
553
- const updatedState = proxy.get();
554
- const file1 = updatedState.find(n => n.id === "file1");
555
- expect(file1!.data).toEqual({ name: "NewFile", size: 500 });
556
- });
557
-
558
- it("updateAt() provides convenient partial update by node id", () => {
559
- const initialState: Primitive.TreeState<typeof FolderNode> = [
560
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
561
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
562
- ];
563
- const { env, operations } = createEnvWithState(initialState);
564
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
565
-
566
- // Use updateAt for convenience
567
- proxy.updateAt("file1", FileNode, { name: "QuickUpdate" });
568
-
569
- expect(operations).toHaveLength(1);
570
- expect(operations[0]!.kind).toBe("string.set");
571
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
572
- expect(operations[0]!.payload).toBe("QuickUpdate");
573
- });
574
-
575
- it("updateAt() throws for wrong node type", () => {
576
- const initialState: Primitive.TreeState<typeof FolderNode> = [
577
- { id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
578
- ];
579
- const { env } = createEnvWithState(initialState);
580
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
581
-
582
- // Trying to update a file node as a folder should throw
583
- expect(() => proxy.updateAt("file1", FolderNode, { name: "NewName" })).toThrow(
584
- Primitive.ValidationError
585
- );
586
- });
587
-
588
- it("updateAt() throws for non-existent node", () => {
589
- const initialState: Primitive.TreeState<typeof FolderNode> = [
590
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
591
- ];
592
- const { env } = createEnvWithState(initialState);
593
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
594
-
595
- expect(() => proxy.updateAt("nonexistent", FileNode, { name: "Name" })).toThrow(
596
- Primitive.ValidationError
597
- );
598
- });
599
-
600
- it("data.update() on at() proxy also works for partial updates", () => {
601
- const initialState: Primitive.TreeState<typeof FolderNode> = [
602
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
603
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
604
- ];
605
- const { env, operations } = createEnvWithState(initialState);
606
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
607
-
608
- // The at() method returns the data proxy which has update()
609
- proxy.at("file1", FileNode).update({ size: 999 });
610
-
611
- expect(operations).toHaveLength(1);
612
- expect(operations[0]!.kind).toBe("number.set");
613
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
614
- });
615
-
616
- it("update() removes optional node data key when value is undefined", () => {
617
- const initialState: Primitive.TreeState<typeof FolderNode> = [
618
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
619
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
620
- ];
621
- const { env, operations } = createEnvWithState(initialState);
622
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
623
-
624
- proxy.node("file1")!.as(FileNode).update({ size: undefined });
625
-
626
- expect(operations).toHaveLength(1);
627
- expect(operations[0]!.kind).toBe("struct.unset");
628
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
629
-
630
- const file1 = proxy.get().find((n) => n.id === "file1")!;
631
- expect(file1.data).toEqual({ name: "File1" });
632
- expect(hasOwn(file1.data, "size")).toBe(false);
633
- });
634
-
635
- it("updateAt() removes optional node data key when value is null", () => {
636
- const initialState: Primitive.TreeState<typeof FolderNode> = [
637
- { id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
638
- { id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
639
- ];
640
- const { env, operations } = createEnvWithState(initialState);
641
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
642
-
643
- (proxy as any).updateAt("file1", FileNode, { size: null });
644
-
645
- expect(operations).toHaveLength(1);
646
- expect(operations[0]!.kind).toBe("struct.unset");
647
- expect(operations[0]!.path.toTokens()).toEqual(["file1", "size"]);
648
-
649
- const file1 = proxy.get().find((n) => n.id === "file1")!;
650
- expect(file1.data).toEqual({ name: "File1" });
651
- expect(hasOwn(file1.data, "size")).toBe(false);
652
- });
653
-
654
- it("throws when required node data field is updated with undefined", () => {
655
- const StrictFileNode = Primitive.TreeNode("strict-file", {
656
- data: Primitive.Struct({
657
- name: Primitive.String().required(),
658
- note: Primitive.String(),
659
- }),
660
- children: [] as const,
661
- });
662
- const StrictFolderNode = Primitive.TreeNode("strict-folder", {
663
- data: Primitive.Struct({ label: Primitive.String() }),
664
- children: [StrictFileNode] as const,
665
- });
666
- const strictTree = Primitive.Tree({ root: StrictFolderNode });
667
- const initialState: Primitive.TreeState<typeof StrictFolderNode> = [
668
- { id: "root", type: "strict-folder", parentId: null, pos: "a0", data: { label: "Root" } },
669
- { id: "file1", type: "strict-file", parentId: "root", pos: "a0", data: { name: "File1", note: "keep" } },
670
- ];
671
- const operations: Operation.Operation<any, any, any>[] = [];
672
- const env = ProxyEnvironment.make({
673
- onOperation: (op) => {
674
- operations.push(op);
675
- },
676
- getState: () => initialState,
677
- });
678
- const proxy = strictTree._internal.createProxy(env, OperationPath.make(""));
679
-
680
- expect(() => proxy.node("file1")!.as(StrictFileNode).update({ name: undefined as never })).toThrow(
681
- Primitive.ValidationError
682
- );
683
- expect(operations).toHaveLength(0);
684
- });
685
-
686
- it("throws when required node data field is updated with null", () => {
687
- const StrictFileNode = Primitive.TreeNode("strict-file", {
688
- data: Primitive.Struct({
689
- name: Primitive.String().required(),
690
- note: Primitive.String(),
691
- }),
692
- children: [] as const,
693
- });
694
- const StrictFolderNode = Primitive.TreeNode("strict-folder", {
695
- data: Primitive.Struct({ label: Primitive.String() }),
696
- children: [StrictFileNode] as const,
697
- });
698
- const strictTree = Primitive.Tree({ root: StrictFolderNode });
699
- const initialState: Primitive.TreeState<typeof StrictFolderNode> = [
700
- { id: "root", type: "strict-folder", parentId: null, pos: "a0", data: { label: "Root" } },
701
- { id: "file1", type: "strict-file", parentId: "root", pos: "a0", data: { name: "File1", note: "keep" } },
702
- ];
703
- const operations: Operation.Operation<any, any, any>[] = [];
704
- const env = ProxyEnvironment.make({
705
- onOperation: (op) => {
706
- operations.push(op);
707
- },
708
- getState: () => initialState,
709
- });
710
- const proxy = strictTree._internal.createProxy(env, OperationPath.make(""));
711
-
712
- expect(() => (proxy as any).updateAt("file1", StrictFileNode, { name: null })).toThrow(
713
- Primitive.ValidationError
714
- );
715
- expect(operations).toHaveLength(0);
716
- });
717
- });
718
-
719
- describe("proxy - insert with defaults", () => {
720
- // Define node types with defaults
721
- const ItemNodeWithDefaults = Primitive.TreeNode("item", {
722
- data: Primitive.Struct({
723
- title: Primitive.String().required(), // Must provide
724
- count: Primitive.Number().default(0), // Has default, optional
725
- active: Primitive.Boolean().default(true), // Has default, optional
726
- }),
727
- children: [] as const,
728
- });
729
-
730
- const ContainerNodeWithDefaults = Primitive.TreeNode("container", {
731
- data: Primitive.Struct({
732
- name: Primitive.String().required(), // Must provide
733
- }),
734
- children: (): readonly Primitive.AnyTreeNodePrimitive[] => [ContainerNodeWithDefaults, ItemNodeWithDefaults],
735
- });
736
-
737
- const treeWithDefaults = Primitive.Tree({
738
- root: ContainerNodeWithDefaults,
739
- });
740
-
741
- // Helper to create a mock environment
742
- const createEnvWithDefaultsTree = (
743
- state: Primitive.TreeState<typeof ContainerNodeWithDefaults> = []
744
- ): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
745
- const operations: Operation.Operation<any, any, any>[] = [];
746
- let currentState = [...state] as Primitive.TreeState<typeof ContainerNodeWithDefaults>;
747
- let idCounter = 0;
748
-
749
- const env = ProxyEnvironment.make({
750
- onOperation: (op) => {
751
- operations.push(op);
752
- currentState = treeWithDefaults._internal.applyOperation(currentState, op);
753
- },
754
- getState: () => currentState,
755
- generateId: () => `node-${++idCounter}`,
756
- });
757
-
758
- return { env, operations };
759
- };
760
-
761
- it("insertFirst() only requires fields without defaults", () => {
762
- const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
763
- { id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
764
- ];
765
- const { env, operations } = createEnvWithDefaultsTree(initialState);
766
- const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
767
-
768
- // Only provide required field 'title', count and active should use defaults
769
- const newId = proxy.insertFirst("root", ItemNodeWithDefaults, { title: "New Item" });
770
-
771
- expect(operations).toHaveLength(1);
772
- expect(operations[0]!.kind).toBe("tree.insert");
773
- expect(newId).toBe("node-1");
774
-
775
- const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
776
- expect(payload.data.title).toBe("New Item");
777
- expect(payload.data.count).toBe(0); // Default value
778
- expect(payload.data.active).toBe(true); // Default value
779
- });
780
-
781
- it("insertLast() applies defaults for omitted fields", () => {
782
- const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
783
- { id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
784
- ];
785
- const { env, operations } = createEnvWithDefaultsTree(initialState);
786
- const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
787
-
788
- // Provide title and override count, let active use default
789
- proxy.insertLast("root", ItemNodeWithDefaults, { title: "Item", count: 42 });
790
-
791
- const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
792
- expect(payload.data.title).toBe("Item");
793
- expect(payload.data.count).toBe(42); // Overridden
794
- expect(payload.data.active).toBe(true); // Default value
795
- });
796
-
797
- it("insertAt() allows omitting all optional fields with defaults", () => {
798
- const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
799
- { id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
800
- ];
801
- const { env, operations } = createEnvWithDefaultsTree(initialState);
802
- const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
803
-
804
- // Only provide required 'name' for container
805
- proxy.insertAt("root", 0, ContainerNodeWithDefaults, { name: "Subfolder" });
806
-
807
- const payload = operations[0]!.payload as { type: string; data: { name: string } };
808
- expect(payload.type).toBe("container");
809
- expect(payload.data.name).toBe("Subfolder");
810
- });
811
-
812
- it("insertAfter() uses defaults when fields are omitted", () => {
813
- const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
814
- { id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
815
- { id: "item1", type: "item", parentId: "root", pos: "a0", data: { title: "First", count: 1, active: false } },
816
- ];
817
- const { env, operations } = createEnvWithDefaultsTree(initialState);
818
- const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
819
-
820
- // Insert after sibling with only required field
821
- proxy.insertAfter("item1", ItemNodeWithDefaults, { title: "Second" });
822
-
823
- const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
824
- expect(payload.data.title).toBe("Second");
825
- expect(payload.data.count).toBe(0); // Default
826
- expect(payload.data.active).toBe(true); // Default
827
- });
828
-
829
- it("insertBefore() uses defaults when fields are omitted", () => {
830
- const initialState: Primitive.TreeState<typeof ContainerNodeWithDefaults> = [
831
- { id: "root", type: "container", parentId: null, pos: "a0", data: { name: "Root" } },
832
- { id: "item1", type: "item", parentId: "root", pos: "a0", data: { title: "First", count: 1, active: false } },
833
- ];
834
- const { env, operations } = createEnvWithDefaultsTree(initialState);
835
- const proxy = treeWithDefaults._internal.createProxy(env, OperationPath.make(""));
836
-
837
- // Insert before sibling with only required field, override active
838
- proxy.insertBefore("item1", ItemNodeWithDefaults, { title: "Zeroth", active: false });
839
-
840
- const payload = operations[0]!.payload as { data: { title: string; count: number; active: boolean } };
841
- expect(payload.data.title).toBe("Zeroth");
842
- expect(payload.data.count).toBe(0); // Default
843
- expect(payload.data.active).toBe(false); // Overridden
844
- });
845
- });
846
- });
847
-
848
- describe("TreePrimitive - nested input for set() and default()", () => {
849
- // Define node types using the new TreeNode API
850
- // Using TreeNodeSelf for self-referential nodes preserves type safety
851
- const FileNode = Primitive.TreeNode("file", {
852
- data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number().default(0) }),
853
- children: [] as const,
854
- });
855
-
856
- const FolderNode = Primitive.TreeNode("folder", {
857
- data: Primitive.Struct({ name: Primitive.String() }),
858
- children: [Primitive.TreeNodeSelf, FileNode],
859
- });
860
-
861
- const fileSystemTree = Primitive.Tree({
862
- root: FolderNode,
863
- });
864
-
865
- // Helper to create a mock environment with state access
866
- const createEnvWithState = (
867
- state: Primitive.TreeState<typeof FolderNode> = []
868
- ): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
869
- const operations: Operation.Operation<any, any, any>[] = [];
870
- let currentState = [...state] as Primitive.TreeState<typeof FolderNode>;
871
- let idCounter = 0;
872
-
873
- const env = ProxyEnvironment.make({
874
- onOperation: (op) => {
875
- operations.push(op);
876
- // Apply operation to keep state in sync
877
- currentState = fileSystemTree._internal.applyOperation(currentState, op);
878
- },
879
- getState: () => currentState,
880
- generateId: () => `node-${++idCounter}`,
881
- });
882
-
883
- return { env, operations };
884
- };
885
-
886
- describe("set() with nested input", () => {
887
- it("converts nested input to flat TreeState", () => {
888
- const { env, operations } = createEnvWithState();
889
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
890
-
891
- proxy.set({
892
- type: "folder",
893
- name: "Root",
894
- children: [
895
- { type: "file", name: "file1.txt", children: [] },
896
- { type: "file", name: "file2.txt", children: [] },
897
- ],
898
- });
899
-
900
- expect(operations).toHaveLength(1);
901
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
902
- expect(payload).toHaveLength(3);
903
-
904
- // Root
905
- expect(payload[0]!.type).toBe("folder");
906
- expect(payload[0]!.parentId).toBe(null);
907
- expect(payload[0]!.data).toEqual({ name: "Root" });
908
-
909
- // First file child
910
- expect(payload[1]!.type).toBe("file");
911
- expect(payload[1]!.parentId).toBe(payload[0]!.id);
912
- expect(payload[1]!.data).toEqual({ name: "file1.txt", size: 0 }); // size has default
913
-
914
- // Second file child
915
- expect(payload[2]!.type).toBe("file");
916
- expect(payload[2]!.parentId).toBe(payload[0]!.id);
917
- expect(payload[2]!.data).toEqual({ name: "file2.txt", size: 0 });
918
-
919
- // Positions should be in order
920
- expect(payload[1]!.pos < payload[2]!.pos).toBe(true);
921
- });
922
-
923
- it("allows explicit IDs in nested input", () => {
924
- const { env, operations } = createEnvWithState();
925
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
926
-
927
- proxy.set({
928
- type: "folder",
929
- id: "my-root",
930
- name: "Root",
931
- children: [
932
- { type: "file", id: "my-file", name: "file.txt", children: [] },
933
- ],
934
- });
935
-
936
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
937
- expect(payload[0]!.id).toBe("my-root");
938
- expect(payload[1]!.id).toBe("my-file");
939
- });
940
-
941
- it("throws on duplicate IDs", () => {
942
- const { env } = createEnvWithState();
943
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
944
-
945
- expect(() =>
946
- proxy.set({
947
- type: "folder",
948
- id: "dup",
949
- name: "Root",
950
- children: [
951
- { type: "file", id: "dup", name: "file.txt", children: [] },
952
- ],
953
- })
954
- ).toThrow(Primitive.ValidationError);
955
- });
956
-
957
- it("validates child types against schema", () => {
958
- const { env } = createEnvWithState();
959
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
960
-
961
- // File nodes cannot have children with type "folder"
962
- // But since we can't test FileNode as root (wrong type), let's test folder with wrong child
963
- // Actually FolderNode accepts both FolderNode and FileNode, so this won't fail on types
964
- // Let's test wrong root type instead - but the input is typed, so this is caught at compile time
965
- // We can test runtime by forcing wrong type
966
- const invalidInput = {
967
- type: "file" as "folder", // Cast to bypass type check
968
- name: "WrongRoot",
969
- children: [],
970
- };
971
-
972
- expect(() => proxy.set(invalidInput as any)).toThrow(Primitive.ValidationError);
973
- });
974
-
975
- it("handles deeply nested structures", () => {
976
- const { env, operations } = createEnvWithState();
977
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
978
-
979
- proxy.set({
980
- type: "folder",
981
- name: "Root",
982
- children: [
983
- {
984
- type: "folder" as const,
985
- name: "Level1",
986
- children: [
987
- {
988
- type: "folder",
989
- name: "Level2",
990
- children: [
991
- { type: "file", name: "deep.txt", children: [] },
992
- ],
993
- },
994
- ],
995
- },
996
- ],
997
- });
998
-
999
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
1000
- expect(payload).toHaveLength(4);
1001
-
1002
- // Verify parent chain (cast data to access name since TreeNodeState.data is unknown)
1003
- const root = payload.find(n => (n.data as { name: string }).name === "Root");
1004
- const level1 = payload.find(n => (n.data as { name: string }).name === "Level1");
1005
- const level2 = payload.find(n => (n.data as { name: string }).name === "Level2");
1006
- const deep = payload.find(n => (n.data as { name: string }).name === "deep.txt");
1007
-
1008
- expect(root!.parentId).toBe(null);
1009
- expect(level1!.parentId).toBe(root!.id);
1010
- expect(level2!.parentId).toBe(level1!.id);
1011
- expect(deep!.parentId).toBe(level2!.id);
1012
- });
1013
-
1014
- it("applies data defaults to nested input", () => {
1015
- const { env, operations } = createEnvWithState();
1016
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
1017
-
1018
- proxy.set({
1019
- type: "folder",
1020
- name: "Root",
1021
- children: [
1022
- { type: "file", name: "file.txt", children: [] }, // size omitted, should use default
1023
- ],
1024
- });
1025
-
1026
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
1027
- const fileNode = payload.find(n => n.type === "file");
1028
- expect((fileNode!.data as any).size).toBe(0); // Default value
1029
- });
1030
-
1031
- it("prunes optional keys explicitly set to undefined in nested input", () => {
1032
- const { env, operations } = createEnvWithState();
1033
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
1034
-
1035
- proxy.set({
1036
- type: "folder",
1037
- name: "Root",
1038
- children: [
1039
- { type: "file", name: "file.txt", size: undefined, children: [] },
1040
- ],
1041
- });
1042
-
1043
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
1044
- const fileNode = payload.find(n => n.type === "file");
1045
- expect(fileNode).toBeDefined();
1046
- expect(fileNode!.data).toEqual({ name: "file.txt" });
1047
- expect(hasOwn(fileNode!.data, "size")).toBe(false);
1048
- });
1049
-
1050
- it("prunes optional keys explicitly set to null in nested input", () => {
1051
- const { env, operations } = createEnvWithState();
1052
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
1053
-
1054
- (proxy as any).set({
1055
- type: "folder",
1056
- name: "Root",
1057
- children: [
1058
- { type: "file", name: "file.txt", size: null, children: [] },
1059
- ],
1060
- });
1061
-
1062
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
1063
- const fileNode = payload.find(n => n.type === "file");
1064
- expect(fileNode).toBeDefined();
1065
- expect(fileNode!.data).toEqual({ name: "file.txt" });
1066
- expect(hasOwn(fileNode!.data, "size")).toBe(false);
1067
- });
1068
- });
1069
-
1070
- describe("default() with nested input", () => {
1071
- it("creates initial state from nested default", () => {
1072
- const treeWithDefault = fileSystemTree.default({
1073
- type: "folder",
1074
- id: "default-root",
1075
- name: "Default Root",
1076
- children: [
1077
- { type: "file", id: "default-file", name: "readme.txt", children: [] },
1078
- ],
1079
- });
1080
-
1081
- const initialState = treeWithDefault._internal.getInitialState();
1082
- expect(initialState).toHaveLength(2);
1083
-
1084
- const root = initialState!.find(n => n.id === "default-root");
1085
- const file = initialState!.find(n => n.id === "default-file");
1086
-
1087
- expect(root!.type).toBe("folder");
1088
- expect(root!.parentId).toBe(null);
1089
- expect(root!.data).toEqual({ name: "Default Root" });
1090
-
1091
- expect(file!.type).toBe("file");
1092
- expect(file!.parentId).toBe("default-root");
1093
- expect((file!.data as any).size).toBe(0); // Default applied
1094
- });
1095
-
1096
- it("generates IDs for default when not provided", () => {
1097
- const treeWithDefault = fileSystemTree.default({
1098
- type: "folder",
1099
- name: "Root",
1100
- children: [],
1101
- });
1102
-
1103
- const initialState = treeWithDefault._internal.getInitialState();
1104
- expect(initialState).toHaveLength(1);
1105
- expect(typeof initialState![0]!.id).toBe("string");
1106
- expect(initialState![0]!.id.length).toBeGreaterThan(0);
1107
- });
1108
- });
1109
-
1110
- describe("sibling ordering", () => {
1111
- it("maintains children order with correct positions", () => {
1112
- const { env, operations } = createEnvWithState();
1113
- const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
1114
-
1115
- proxy.set({
1116
- type: "folder",
1117
- name: "Root",
1118
- children: [
1119
- { type: "file", id: "first", name: "a.txt", children: [] },
1120
- { type: "file", id: "second", name: "b.txt", children: [] },
1121
- { type: "file", id: "third", name: "c.txt", children: [] },
1122
- ],
1123
- });
1124
-
1125
- const payload = operations[0]!.payload as Primitive.TreeState<typeof FolderNode>;
1126
- const first = payload.find(n => n.id === "first")!;
1127
- const second = payload.find(n => n.id === "second")!;
1128
- const third = payload.find(n => n.id === "third")!;
1129
-
1130
- // Positions should be in ascending order
1131
- expect(first.pos < second.pos).toBe(true);
1132
- expect(second.pos < third.pos).toBe(true);
1133
- });
1134
- });
1135
- });
1136
-
1137
- // =============================================================================
1138
- // Integration Tests - Tree with Complex Structures
1139
- // =============================================================================