@highstate/cli 0.9.15 → 0.9.18

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.
@@ -0,0 +1,489 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { applySchemaTransformations } from "./schema-transformer"
3
+
4
+ describe("applySchemaTransformations", () => {
5
+ it("should transform simple values to entity/schema structure", async () => {
6
+ const input = `
7
+ const spec = {
8
+ inputs: {
9
+ /**
10
+ * The Kubernetes cluster to deploy on.
11
+ */
12
+ cluster: clusterEntity,
13
+ },
14
+ args: {
15
+ /**
16
+ * The port number to use.
17
+ */
18
+ port: Type.Number(),
19
+ },
20
+ }`
21
+
22
+ const result = await applySchemaTransformations(input)
23
+
24
+ expect(result).toContain("entity: clusterEntity")
25
+ expect(result).toContain("schema: Type.Number()")
26
+ expect(result).toContain("description: `The Kubernetes cluster to deploy on.`")
27
+ expect(result).toContain("description: `The port number to use.`")
28
+ })
29
+
30
+ it("should inject description into existing meta field", async () => {
31
+ const input = `
32
+ const spec = {
33
+ args: {
34
+ /**
35
+ * The API token for authentication.
36
+ */
37
+ token: {
38
+ schema: Type.String(),
39
+ meta: {
40
+ displayName: "API Token",
41
+ sensitive: true,
42
+ },
43
+ },
44
+ },
45
+ }`
46
+
47
+ const result = await applySchemaTransformations(input)
48
+
49
+ expect(result).toContain('displayName: "API Token"')
50
+ expect(result).toContain("sensitive: true")
51
+ expect(result).toContain("description: `The API token for authentication.`")
52
+ expect(result).not.toContain("...obj")
53
+ expect(result).not.toContain("((obj) =>")
54
+ })
55
+
56
+ it("should add meta field if it doesn't exist in structured object", async () => {
57
+ const input = `
58
+ const spec = {
59
+ inputs: {
60
+ /**
61
+ * The target endpoint.
62
+ */
63
+ endpoint: {
64
+ entity: endpointEntity,
65
+ required: false,
66
+ },
67
+ },
68
+ }`
69
+
70
+ const result = await applySchemaTransformations(input)
71
+
72
+ expect(result).toContain("entity: endpointEntity")
73
+ expect(result).toContain("required: false")
74
+ expect(result).toContain("meta: {")
75
+ expect(result).toContain("description: `The target endpoint.`")
76
+ })
77
+
78
+ it("should handle $args marker function", async () => {
79
+ const input = `
80
+ const spec = {
81
+ args: $args({
82
+ /**
83
+ * The configuration file path.
84
+ */
85
+ configPath: Type.String(),
86
+ }),
87
+ }`
88
+
89
+ const result = await applySchemaTransformations(input)
90
+
91
+ expect(result).toContain("schema: Type.String()")
92
+ expect(result).toContain("description: `The configuration file path.`")
93
+ })
94
+
95
+ it("should handle $inputs marker function", async () => {
96
+ const input = `
97
+ const spec = {
98
+ inputs: $inputs({
99
+ /**
100
+ * The source data.
101
+ */
102
+ source: dataEntity,
103
+ }),
104
+ }`
105
+
106
+ const result = await applySchemaTransformations(input)
107
+
108
+ expect(result).toContain("entity: dataEntity")
109
+ expect(result).toContain("description: `The source data.`")
110
+ })
111
+
112
+ it("should handle $outputs marker function", async () => {
113
+ const input = `
114
+ const spec = {
115
+ outputs: $outputs({
116
+ /**
117
+ * The processed result.
118
+ */
119
+ result: resultEntity,
120
+ }),
121
+ }`
122
+
123
+ const result = await applySchemaTransformations(input)
124
+
125
+ expect(result).toContain("entity: resultEntity")
126
+ expect(result).toContain("description: `The processed result.`")
127
+ })
128
+
129
+ it("should handle $secrets marker function", async () => {
130
+ const input = `
131
+ const spec = {
132
+ secrets: $secrets({
133
+ /**
134
+ * The database password.
135
+ */
136
+ dbPassword: Type.String(),
137
+ }),
138
+ }`
139
+
140
+ const result = await applySchemaTransformations(input)
141
+
142
+ expect(result).toContain("schema: Type.String()")
143
+ expect(result).toContain("description: `The database password.`")
144
+ })
145
+
146
+ it("should ignore properties without JSDoc comments", async () => {
147
+ const input = `
148
+ const spec = {
149
+ inputs: {
150
+ cluster: clusterEntity,
151
+ /**
152
+ * Only this one has a comment.
153
+ */
154
+ endpoint: endpointEntity,
155
+ },
156
+ }`
157
+
158
+ const result = await applySchemaTransformations(input)
159
+
160
+ expect(result).toContain("cluster: clusterEntity") // unchanged
161
+ expect(result).toContain("entity: endpointEntity") // transformed
162
+ expect(result).toContain("description: `Only this one has a comment.`")
163
+ })
164
+
165
+ it("should ignore properties not in target objects", async () => {
166
+ const input = `
167
+ const config = {
168
+ /**
169
+ * This should not be transformed.
170
+ */
171
+ someProperty: "value",
172
+ }
173
+
174
+ const spec = {
175
+ inputs: {
176
+ /**
177
+ * This should be transformed.
178
+ */
179
+ cluster: clusterEntity,
180
+ },
181
+ }`
182
+
183
+ const result = await applySchemaTransformations(input)
184
+
185
+ expect(result).toContain('someProperty: "value"') // unchanged
186
+ expect(result).toContain("entity: clusterEntity") // transformed
187
+ })
188
+
189
+ it("should clean JSDoc comments properly", async () => {
190
+ const input = `
191
+ const spec = {
192
+ args: {
193
+ /**
194
+ * This is a description with \`backticks\` and \${template} literals.
195
+ * It also has multiple lines.
196
+ */
197
+ value: Type.String(),
198
+ },
199
+ }`
200
+
201
+ const result = await applySchemaTransformations(input)
202
+
203
+ expect(result).toContain("schema: Type.String()")
204
+ expect(result).toContain(
205
+ "description: `This is a description with \\`backticks\\` and \\${template} literals.",
206
+ )
207
+ expect(result).toContain("It also has multiple lines.`")
208
+ })
209
+
210
+ it("should add .meta() to z.object fields with JSDoc comments", async () => {
211
+ const input = `
212
+ const userSchema = z.object({
213
+ /**
214
+ * The user's unique identifier.
215
+ */
216
+ id: z.string(),
217
+ /**
218
+ * The user's email address.
219
+ */
220
+ email: z.string().email(),
221
+ name: z.string(), // no comment, should not be transformed
222
+ })`
223
+
224
+ const result = await applySchemaTransformations(input)
225
+
226
+ // Should add import at the top
227
+ expect(result).toContain(
228
+ 'import { camelCaseToHumanReadable as __camelCaseToHumanReadable } from "@highstate/contract"',
229
+ )
230
+
231
+ // Should add .meta() with both title and description
232
+ expect(result).toContain(
233
+ 'id: z.string().meta({ title: __camelCaseToHumanReadable("id"), description: `The user\'s unique identifier.` })',
234
+ )
235
+ expect(result).toContain(
236
+ 'email: z.string().email().meta({ title: __camelCaseToHumanReadable("email"), description: `The user\'s email address.` })',
237
+ )
238
+ expect(result).toContain("name: z.string(), // no comment") // unchanged
239
+ })
240
+
241
+ it("should not add .meta() if already present in z.object fields", async () => {
242
+ const input = `
243
+ const userSchema = z.object({
244
+ /**
245
+ * The user's identifier.
246
+ */
247
+ id: z.string().meta({ displayName: "ID" }),
248
+ })`
249
+
250
+ const result = await applySchemaTransformations(input)
251
+
252
+ // Should not modify the field that already has .meta()
253
+ expect(result).toContain('id: z.string().meta({ displayName: "ID" })')
254
+ expect(result).not.toContain("description:")
255
+ })
256
+
257
+ it("should handle nested z.object patterns", async () => {
258
+ const input = `
259
+ const schema = z.object({
260
+ user: z.object({
261
+ /**
262
+ * The user's name.
263
+ */
264
+ name: z.string(),
265
+ profile: z.object({
266
+ /**
267
+ * The user's age.
268
+ */
269
+ age: z.number(),
270
+ }),
271
+ }),
272
+ })`
273
+
274
+ const result = await applySchemaTransformations(input)
275
+
276
+ expect(result).toContain(
277
+ 'name: z.string().meta({ title: __camelCaseToHumanReadable("name"), description: `The user\'s name.` })',
278
+ )
279
+ expect(result).toContain(
280
+ 'age: z.number().meta({ title: __camelCaseToHumanReadable("age"), description: `The user\'s age.` })',
281
+ )
282
+ })
283
+
284
+ it("should handle multiple JSDoc-commented fields in the same z.object without conflicts", async () => {
285
+ const input = `
286
+ const networkSchema = z.discriminatedUnion("type", [
287
+ z.object({
288
+ type: z.literal("dhcp"),
289
+ }),
290
+ z.object({
291
+ type: z.literal("static"),
292
+
293
+ /**
294
+ * The IPv4 address to assign to the virtual machine.
295
+ */
296
+ address: z.string().optional(),
297
+
298
+ /**
299
+ * The CIDR prefix for the IPv4 address.
300
+ *
301
+ * By default, this is set to 24.
302
+ */
303
+ prefix: z.number().default(24),
304
+
305
+ /**
306
+ * The IPv4 gateway for the virtual machine.
307
+ *
308
+ * If not specified, will be set to the first address in the subnet.
309
+ */
310
+ gateway: z.string().optional(),
311
+ }),
312
+ ]).default({ type: "dhcp" })`
313
+
314
+ const result = await applySchemaTransformations(input)
315
+
316
+ expect(result).toContain(
317
+ 'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
318
+ )
319
+ expect(result).toContain(
320
+ 'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.',
321
+ )
322
+ expect(result).toContain("By default, this is set to 24.` })")
323
+ expect(result).toContain(
324
+ 'gateway: z.string().optional().meta({ title: __camelCaseToHumanReadable("gateway"), description: `The IPv4 gateway for the virtual machine.',
325
+ )
326
+ expect(result).toContain(
327
+ "If not specified, will be set to the first address in the subnet.` })",
328
+ )
329
+ expect(result).toContain('type: z.literal("static"),') // unchanged, no comment
330
+ })
331
+
332
+ it("should handle z.object fields inside z.discriminatedUnion", async () => {
333
+ const input = `
334
+ const schema = z.discriminatedUnion("type", [
335
+ z.object({
336
+ type: z.literal("dhcp"),
337
+ }),
338
+ z.object({
339
+ type: z.literal("static"),
340
+ /**
341
+ * The IPv4 address to assign to the virtual machine.
342
+ */
343
+ address: z.string().optional(),
344
+ /**
345
+ * The CIDR prefix for the IPv4 address.
346
+ */
347
+ prefix: z.number().default(24),
348
+ }),
349
+ ]).default({ type: "dhcp" })`
350
+
351
+ const result = await applySchemaTransformations(input)
352
+
353
+ expect(result).toContain(
354
+ 'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
355
+ )
356
+ expect(result).toContain(
357
+ 'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.` })',
358
+ )
359
+ expect(result).toContain('type: z.literal("dhcp"),') // unchanged, no comment
360
+ expect(result).toContain('type: z.literal("static"),') // unchanged, no comment
361
+ })
362
+
363
+ it("should handle the full defineUnit case with discriminated union and mixed transformations", async () => {
364
+ const input = `
365
+ var virtualMachine = defineUnit({
366
+ type: "proxmox.virtual-machine",
367
+ args: {
368
+ nodeName: z.string().optional(),
369
+ cpuType: z.string().default("host"),
370
+ cores: z.number().default(1),
371
+ sockets: z.number().default(1),
372
+ memory: z.number().default(512),
373
+ /**
374
+ * The IPv4 address configuration for the virtual machine.
375
+ */
376
+ ipv4: {
377
+ schema: z.discriminatedUnion("type", [
378
+ z.object({
379
+ type: z.literal("dhcp")
380
+ }),
381
+ z.object({
382
+ type: z.literal("static"),
383
+ /**
384
+ * The IPv4 address to assign to the virtual machine.
385
+ */
386
+ address: z.string().optional(),
387
+ /**
388
+ * The CIDR prefix for the IPv4 address.
389
+ *
390
+ * By default, this is set to 24.
391
+ */
392
+ prefix: z.number().default(24),
393
+ /**
394
+ * The IPv4 gateway for the virtual machine.
395
+ *
396
+ * If not specified, will be set to the first address in the subnet.
397
+ */
398
+ gateway: z.string().optional()
399
+ })
400
+ ]).default({ type: "dhcp" })
401
+ },
402
+ dns: z.string().array().optional(),
403
+ datastoreId: z.string().optional(),
404
+ diskSize: z.number().default(8),
405
+ bridge: z.string().default("vmbr0"),
406
+ sshPort: z.number().default(22),
407
+ sshUser: z.string().default("root"),
408
+ waitForAgent: z.boolean().default(true),
409
+ vendorData: z.string().optional()
410
+ },
411
+ secrets: {
412
+ sshPassword: z.string().optional()
413
+ },
414
+ inputs: {
415
+ proxmoxCluster: clusterEntity,
416
+ image: imageEntity,
417
+ sshKeyPair: {
418
+ entity: keyPairEntity,
419
+ required: false
420
+ },
421
+ /**
422
+ * The cloud-init vendor data to use for the virtual machine.
423
+ *
424
+ * You can provide a cloud-config from the distribution component.
425
+ */
426
+ vendorData: {
427
+ entity: fileEntity,
428
+ required: false,
429
+ }
430
+ },
431
+ outputs: serverOutputs,
432
+ meta: {
433
+ title: "Proxmox Virtual Machine",
434
+ description: "The virtual machine on a Proxmox cluster.",
435
+ category: "Proxmox",
436
+ icon: "simple-icons:proxmox",
437
+ iconColor: "#e56901",
438
+ secondaryIcon: "codicon:vm"
439
+ },
440
+ source: {
441
+ package: "@highstate/proxmox",
442
+ path: "virtual-machine"
443
+ }
444
+ });`
445
+
446
+ const result = await applySchemaTransformations(input)
447
+
448
+ // Should transform args.ipv4 to entity/schema structure (existing transformer behavior)
449
+ expect(result).toContain("ipv4: {")
450
+ expect(result).toContain("schema: z.discriminatedUnion")
451
+ expect(result).toContain("meta: {")
452
+ expect(result).toContain(
453
+ "description: `The IPv4 address configuration for the virtual machine.`",
454
+ )
455
+
456
+ // Should add .meta() to z.object fields with JSDoc comments inside the discriminated union
457
+ expect(result).toContain(
458
+ 'address: z.string().optional().meta({ title: __camelCaseToHumanReadable("address"), description: `The IPv4 address to assign to the virtual machine.` })',
459
+ )
460
+ expect(result).toContain(
461
+ 'prefix: z.number().default(24).meta({ title: __camelCaseToHumanReadable("prefix"), description: `The CIDR prefix for the IPv4 address.',
462
+ )
463
+ expect(result).toContain("By default, this is set to 24.` })")
464
+ expect(result).toContain(
465
+ 'gateway: z.string().optional().meta({ title: __camelCaseToHumanReadable("gateway"), description: `The IPv4 gateway for the virtual machine.',
466
+ )
467
+ expect(result).toContain(
468
+ "If not specified, will be set to the first address in the subnet.` })",
469
+ )
470
+
471
+ // Should NOT transform fields without JSDoc comments
472
+ expect(result).toContain("nodeName: z.string().optional(),") // unchanged
473
+ expect(result).toContain('cpuType: z.string().default("host"),') // unchanged
474
+ expect(result).toContain("cores: z.number().default(1),") // unchanged
475
+ expect(result).toContain("dns: z.string().array().optional(),") // unchanged
476
+ expect(result).toContain('type: z.literal("dhcp")') // unchanged
477
+ expect(result).toContain('type: z.literal("static"),') // unchanged
478
+
479
+ // Should properly handle inputs.vendorData with existing meta (no change expected since it already has meta)
480
+ expect(result).toContain("vendorData: {")
481
+ expect(result).toContain("entity: fileEntity,")
482
+ expect(result).toContain("required: false,")
483
+ expect(result).toContain("You can provide a cloud-config from the distribution component.")
484
+
485
+ // Should NOT transform other inputs without JSDoc
486
+ expect(result).toContain("proxmoxCluster: clusterEntity,") // unchanged
487
+ expect(result).toContain("image: imageEntity,") // unchanged
488
+ })
489
+ })