@intentius/chant-lexicon-k8s 0.0.12

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 (123) hide show
  1. package/dist/integrity.json +32 -0
  2. package/dist/manifest.json +8 -0
  3. package/dist/meta.json +1413 -0
  4. package/dist/rules/hardcoded-namespace.ts +56 -0
  5. package/dist/rules/k8s-helpers.ts +149 -0
  6. package/dist/rules/wk8005.ts +59 -0
  7. package/dist/rules/wk8006.ts +68 -0
  8. package/dist/rules/wk8041.ts +73 -0
  9. package/dist/rules/wk8042.ts +48 -0
  10. package/dist/rules/wk8101.ts +65 -0
  11. package/dist/rules/wk8102.ts +42 -0
  12. package/dist/rules/wk8103.ts +45 -0
  13. package/dist/rules/wk8104.ts +69 -0
  14. package/dist/rules/wk8105.ts +45 -0
  15. package/dist/rules/wk8201.ts +55 -0
  16. package/dist/rules/wk8202.ts +46 -0
  17. package/dist/rules/wk8203.ts +46 -0
  18. package/dist/rules/wk8204.ts +54 -0
  19. package/dist/rules/wk8205.ts +56 -0
  20. package/dist/rules/wk8207.ts +45 -0
  21. package/dist/rules/wk8208.ts +45 -0
  22. package/dist/rules/wk8209.ts +45 -0
  23. package/dist/rules/wk8301.ts +51 -0
  24. package/dist/rules/wk8302.ts +46 -0
  25. package/dist/rules/wk8303.ts +96 -0
  26. package/dist/skills/chant-k8s.md +433 -0
  27. package/dist/types/index.d.ts +2934 -0
  28. package/package.json +30 -0
  29. package/src/actions/actions.test.ts +83 -0
  30. package/src/actions/apps.ts +23 -0
  31. package/src/actions/batch.ts +9 -0
  32. package/src/actions/core.ts +62 -0
  33. package/src/actions/index.ts +50 -0
  34. package/src/actions/networking.ts +15 -0
  35. package/src/actions/rbac.ts +13 -0
  36. package/src/codegen/docs-cli.ts +3 -0
  37. package/src/codegen/docs.ts +1147 -0
  38. package/src/codegen/generate-cli.ts +41 -0
  39. package/src/codegen/generate-lexicon.ts +69 -0
  40. package/src/codegen/generate-typescript.ts +97 -0
  41. package/src/codegen/generate.ts +144 -0
  42. package/src/codegen/naming.test.ts +63 -0
  43. package/src/codegen/naming.ts +187 -0
  44. package/src/codegen/package.ts +56 -0
  45. package/src/codegen/patches.ts +108 -0
  46. package/src/codegen/snapshot.test.ts +95 -0
  47. package/src/codegen/typecheck.test.ts +24 -0
  48. package/src/codegen/typecheck.ts +4 -0
  49. package/src/codegen/versions.ts +43 -0
  50. package/src/composites/autoscaled-service.ts +236 -0
  51. package/src/composites/composites.test.ts +1109 -0
  52. package/src/composites/cron-workload.ts +167 -0
  53. package/src/composites/index.ts +14 -0
  54. package/src/composites/namespace-env.ts +163 -0
  55. package/src/composites/node-agent.ts +224 -0
  56. package/src/composites/stateful-app.ts +134 -0
  57. package/src/composites/web-app.ts +180 -0
  58. package/src/composites/worker-pool.ts +230 -0
  59. package/src/coverage.test.ts +27 -0
  60. package/src/coverage.ts +35 -0
  61. package/src/crd/loader.ts +112 -0
  62. package/src/crd/parser.test.ts +217 -0
  63. package/src/crd/parser.ts +279 -0
  64. package/src/crd/types.ts +54 -0
  65. package/src/default-labels.test.ts +111 -0
  66. package/src/default-labels.ts +122 -0
  67. package/src/generated/index.d.ts +2934 -0
  68. package/src/generated/index.ts +203 -0
  69. package/src/generated/lexicon-k8s.json +1413 -0
  70. package/src/generated/runtime.ts +4 -0
  71. package/src/import/generator.test.ts +121 -0
  72. package/src/import/generator.ts +285 -0
  73. package/src/import/parser.test.ts +156 -0
  74. package/src/import/parser.ts +204 -0
  75. package/src/import/roundtrip.test.ts +86 -0
  76. package/src/index.ts +38 -0
  77. package/src/lint/post-synth/k8s-helpers.test.ts +219 -0
  78. package/src/lint/post-synth/k8s-helpers.ts +149 -0
  79. package/src/lint/post-synth/post-synth.test.ts +969 -0
  80. package/src/lint/post-synth/wk8005.ts +59 -0
  81. package/src/lint/post-synth/wk8006.ts +68 -0
  82. package/src/lint/post-synth/wk8041.ts +73 -0
  83. package/src/lint/post-synth/wk8042.ts +48 -0
  84. package/src/lint/post-synth/wk8101.ts +65 -0
  85. package/src/lint/post-synth/wk8102.ts +42 -0
  86. package/src/lint/post-synth/wk8103.ts +45 -0
  87. package/src/lint/post-synth/wk8104.ts +69 -0
  88. package/src/lint/post-synth/wk8105.ts +45 -0
  89. package/src/lint/post-synth/wk8201.ts +55 -0
  90. package/src/lint/post-synth/wk8202.ts +46 -0
  91. package/src/lint/post-synth/wk8203.ts +46 -0
  92. package/src/lint/post-synth/wk8204.ts +54 -0
  93. package/src/lint/post-synth/wk8205.ts +56 -0
  94. package/src/lint/post-synth/wk8207.ts +45 -0
  95. package/src/lint/post-synth/wk8208.ts +45 -0
  96. package/src/lint/post-synth/wk8209.ts +45 -0
  97. package/src/lint/post-synth/wk8301.ts +51 -0
  98. package/src/lint/post-synth/wk8302.ts +46 -0
  99. package/src/lint/post-synth/wk8303.ts +96 -0
  100. package/src/lint/rules/hardcoded-namespace.ts +56 -0
  101. package/src/lint/rules/rules.test.ts +69 -0
  102. package/src/lsp/completions.test.ts +64 -0
  103. package/src/lsp/completions.ts +20 -0
  104. package/src/lsp/hover.test.ts +69 -0
  105. package/src/lsp/hover.ts +68 -0
  106. package/src/package-cli.ts +28 -0
  107. package/src/plugin.test.ts +209 -0
  108. package/src/plugin.ts +915 -0
  109. package/src/serializer.test.ts +275 -0
  110. package/src/serializer.ts +278 -0
  111. package/src/spec/fetch.test.ts +24 -0
  112. package/src/spec/fetch.ts +68 -0
  113. package/src/spec/parse.test.ts +102 -0
  114. package/src/spec/parse.ts +477 -0
  115. package/src/testdata/manifests/configmap.yaml +7 -0
  116. package/src/testdata/manifests/deployment.yaml +22 -0
  117. package/src/testdata/manifests/full-app.yaml +61 -0
  118. package/src/testdata/manifests/secret.yaml +7 -0
  119. package/src/testdata/manifests/service.yaml +15 -0
  120. package/src/validate-cli.ts +21 -0
  121. package/src/validate.test.ts +29 -0
  122. package/src/validate.ts +46 -0
  123. package/src/variables.ts +36 -0
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Runtime factory constructors — re-exported from core.
3
+ */
4
+ export { createResource, createProperty } from "@intentius/chant/runtime";
@@ -0,0 +1,121 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { K8sGenerator } from "./generator";
3
+ import type { TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
4
+
5
+ const generator = new K8sGenerator();
6
+
7
+ function makeIR(resources: ResourceIR[]): TemplateIR {
8
+ return { resources, parameters: [] };
9
+ }
10
+
11
+ describe("K8sGenerator", () => {
12
+ test("generates valid TypeScript from IR", () => {
13
+ const ir = makeIR([
14
+ {
15
+ logicalId: "deploymentApp",
16
+ type: "K8s::Apps::Deployment",
17
+ properties: {
18
+ metadata: { name: "app" },
19
+ spec: { replicas: 2 },
20
+ },
21
+ },
22
+ ]);
23
+ const files = generator.generate(ir);
24
+ expect(files.length).toBe(1);
25
+ expect(files[0].path).toBe("main.ts");
26
+ expect(files[0].content).toContain("import");
27
+ expect(files[0].content).toContain("Deployment");
28
+ });
29
+
30
+ test("imports correct classes", () => {
31
+ const ir = makeIR([
32
+ {
33
+ logicalId: "svc",
34
+ type: "K8s::Core::Service",
35
+ properties: { metadata: { name: "svc" } },
36
+ },
37
+ ]);
38
+ const files = generator.generate(ir);
39
+ expect(files[0].content).toContain(
40
+ 'from "@intentius/chant-lexicon-k8s"',
41
+ );
42
+ expect(files[0].content).toContain("Service");
43
+ });
44
+
45
+ test("uses new Constructor() syntax", () => {
46
+ const ir = makeIR([
47
+ {
48
+ logicalId: "deploy",
49
+ type: "K8s::Apps::Deployment",
50
+ properties: {
51
+ metadata: { name: "app" },
52
+ spec: { replicas: 1 },
53
+ },
54
+ },
55
+ ]);
56
+ const files = generator.generate(ir);
57
+ expect(files[0].content).toContain("new Deployment(");
58
+ });
59
+
60
+ test("handles array properties (containers, env)", () => {
61
+ const ir = makeIR([
62
+ {
63
+ logicalId: "pod",
64
+ type: "K8s::Core::Pod",
65
+ properties: {
66
+ metadata: { name: "pod" },
67
+ spec: {
68
+ containers: [
69
+ { name: "app", image: "nginx:1.0" },
70
+ ],
71
+ },
72
+ },
73
+ },
74
+ ]);
75
+ const files = generator.generate(ir);
76
+ expect(files[0].content).toContain("Container");
77
+ expect(files[0].content).toContain("nginx:1.0");
78
+ });
79
+
80
+ test("generates exports for each resource", () => {
81
+ const ir = makeIR([
82
+ {
83
+ logicalId: "deploy",
84
+ type: "K8s::Apps::Deployment",
85
+ properties: { metadata: { name: "app" } },
86
+ },
87
+ {
88
+ logicalId: "svc",
89
+ type: "K8s::Core::Service",
90
+ properties: { metadata: { name: "svc" } },
91
+ },
92
+ ]);
93
+ const files = generator.generate(ir);
94
+ expect(files[0].content).toContain("export const deploy");
95
+ expect(files[0].content).toContain("export const svc");
96
+ });
97
+
98
+ test("Service ports use ServicePort constructor", () => {
99
+ const ir = makeIR([
100
+ {
101
+ logicalId: "svc",
102
+ type: "K8s::Core::Service",
103
+ properties: {
104
+ metadata: { name: "svc" },
105
+ spec: {
106
+ ports: [{ port: 80, targetPort: 8080 }],
107
+ },
108
+ },
109
+ },
110
+ ]);
111
+ const files = generator.generate(ir);
112
+ expect(files[0].content).toContain("ServicePort");
113
+ });
114
+
115
+ test("handles empty IR", () => {
116
+ const ir = makeIR([]);
117
+ const files = generator.generate(ir);
118
+ expect(files.length).toBe(1);
119
+ // Should still have an import line (even if no resources)
120
+ });
121
+ });
@@ -0,0 +1,285 @@
1
+ /**
2
+ * TypeScript code generator for Kubernetes import.
3
+ *
4
+ * Converts a TemplateIR (from parsed K8s manifests) into TypeScript
5
+ * source code using the @intentius/chant-lexicon-k8s constructors.
6
+ */
7
+
8
+ import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
9
+ import type { TemplateIR, ResourceIR } from "@intentius/chant/import/parser";
10
+
11
+ /**
12
+ * Map K8s entity types to their constructor class names.
13
+ */
14
+ const TYPE_TO_CLASS: Record<string, string> = {
15
+ // Core
16
+ "K8s::Core::Pod": "Pod",
17
+ "K8s::Core::Service": "Service",
18
+ "K8s::Core::ConfigMap": "ConfigMap",
19
+ "K8s::Core::Secret": "Secret",
20
+ "K8s::Core::Namespace": "Namespace",
21
+ "K8s::Core::ServiceAccount": "ServiceAccount",
22
+ "K8s::Core::PersistentVolume": "PersistentVolume",
23
+ "K8s::Core::PersistentVolumeClaim": "PersistentVolumeClaim",
24
+ "K8s::Core::Endpoints": "Endpoints",
25
+ "K8s::Core::LimitRange": "LimitRange",
26
+ "K8s::Core::ResourceQuota": "ResourceQuota",
27
+
28
+ // Apps
29
+ "K8s::Apps::Deployment": "Deployment",
30
+ "K8s::Apps::StatefulSet": "StatefulSet",
31
+ "K8s::Apps::DaemonSet": "DaemonSet",
32
+ "K8s::Apps::ReplicaSet": "ReplicaSet",
33
+
34
+ // Batch
35
+ "K8s::Batch::Job": "Job",
36
+ "K8s::Batch::CronJob": "CronJob",
37
+
38
+ // Networking
39
+ "K8s::Networking::Ingress": "Ingress",
40
+ "K8s::Networking::NetworkPolicy": "NetworkPolicy",
41
+ "K8s::Networking::IngressClass": "IngressClass",
42
+
43
+ // RBAC
44
+ "K8s::Rbac::Role": "Role",
45
+ "K8s::Rbac::ClusterRole": "ClusterRole",
46
+ "K8s::Rbac::RoleBinding": "RoleBinding",
47
+ "K8s::Rbac::ClusterRoleBinding": "ClusterRoleBinding",
48
+
49
+ // Autoscaling
50
+ "K8s::Autoscaling::HorizontalPodAutoscaler": "HorizontalPodAutoscaler",
51
+
52
+ // Policy
53
+ "K8s::Policy::PodDisruptionBudget": "PodDisruptionBudget",
54
+
55
+ // Storage
56
+ "K8s::Storage::StorageClass": "StorageClass",
57
+ };
58
+
59
+ /**
60
+ * Properties that reference known property entity constructors.
61
+ * Key: property name found in K8s specs → Value: constructor class name.
62
+ */
63
+ const PROPERTY_CONSTRUCTORS: Record<string, string> = {
64
+ containers: "Container",
65
+ initContainers: "Container",
66
+ ephemeralContainers: "EphemeralContainer",
67
+ volumes: "Volume",
68
+ volumeMounts: "VolumeMount",
69
+ ports: "ContainerPort",
70
+ env: "EnvVar",
71
+ envFrom: "EnvFromSource",
72
+ resources: "ResourceRequirements",
73
+ securityContext: "SecurityContext",
74
+ podSecurityContext: "PodSecurityContext",
75
+ livenessProbe: "Probe",
76
+ readinessProbe: "Probe",
77
+ startupProbe: "Probe",
78
+ selector: "LabelSelector",
79
+ template: "PodTemplateSpec",
80
+ strategy: "DeploymentStrategy",
81
+ affinity: "Affinity",
82
+ tolerations: "Toleration",
83
+ topologySpreadConstraints: "TopologySpreadConstraint",
84
+ rules: "IngressRule",
85
+ tls: "IngressTLS",
86
+ ingress: "NetworkPolicyIngressRule",
87
+ egress: "NetworkPolicyEgressRule",
88
+ };
89
+
90
+ /**
91
+ * Service port properties need distinct handling since K8s uses "ports"
92
+ * for both ContainerPort and ServicePort depending on the parent resource.
93
+ */
94
+ const SERVICE_PORT_TYPES = new Set([
95
+ "K8s::Core::Service",
96
+ ]);
97
+
98
+ /**
99
+ * Generate TypeScript source code from a Kubernetes IR.
100
+ */
101
+ export class K8sGenerator implements TypeScriptGenerator {
102
+ generate(ir: TemplateIR): GeneratedFile[] {
103
+ const lines: string[] = [];
104
+
105
+ // Collect which constructors are needed
106
+ const usedConstructors = new Set<string>();
107
+ for (const resource of ir.resources) {
108
+ const cls = this.resolveClass(resource.type);
109
+ if (cls) usedConstructors.add(cls);
110
+
111
+ // Check properties for nested constructors
112
+ this.collectNestedConstructors(resource.properties, usedConstructors, resource.type);
113
+ }
114
+
115
+ // Import statement
116
+ const imports = [...usedConstructors].sort().join(", ");
117
+ lines.push(`import { ${imports} } from "@intentius/chant-lexicon-k8s";`);
118
+ lines.push("");
119
+
120
+ // Emit namespaces as comments
121
+ if (ir.metadata?.namespaces && Array.isArray(ir.metadata.namespaces)) {
122
+ lines.push(`// Namespaces: ${(ir.metadata.namespaces as string[]).join(", ")}`);
123
+ lines.push("");
124
+ }
125
+
126
+ // Emit resources
127
+ for (const resource of ir.resources) {
128
+ const cls = this.resolveClass(resource.type);
129
+ if (!cls) {
130
+ lines.push(`// TODO: unsupported type ${resource.type} (${resource.logicalId})`);
131
+ lines.push("");
132
+ continue;
133
+ }
134
+
135
+ const varName = resource.logicalId;
136
+ const propsStr = this.emitProps(resource.properties, 1, resource.type);
137
+
138
+ lines.push(`export const ${varName} = new ${cls}(${propsStr});`);
139
+ lines.push("");
140
+ }
141
+
142
+ return [{ path: "main.ts", content: lines.join("\n") }];
143
+ }
144
+
145
+ /**
146
+ * Resolve a K8s type name to a constructor class name.
147
+ * Falls back to extracting the last segment of the type.
148
+ */
149
+ private resolveClass(type: string): string | undefined {
150
+ if (TYPE_TO_CLASS[type]) return TYPE_TO_CLASS[type];
151
+
152
+ // Fallback: extract Kind from "K8s::Group::Kind"
153
+ const parts = type.split("::");
154
+ if (parts.length === 3 && parts[0] === "K8s") return parts[2];
155
+
156
+ return undefined;
157
+ }
158
+
159
+ private collectNestedConstructors(
160
+ props: Record<string, unknown>,
161
+ used: Set<string>,
162
+ parentType: string,
163
+ ): void {
164
+ for (const [key, value] of Object.entries(props)) {
165
+ if (value === undefined || value === null) continue;
166
+
167
+ // Special case: "ports" on Service resources → ServicePort
168
+ if (key === "ports" && SERVICE_PORT_TYPES.has(parentType)) {
169
+ if (Array.isArray(value)) used.add("ServicePort");
170
+ continue;
171
+ }
172
+
173
+ const constructor = PROPERTY_CONSTRUCTORS[key];
174
+ if (!constructor) continue;
175
+
176
+ if (Array.isArray(value)) {
177
+ used.add(constructor);
178
+ } else if (typeof value === "object") {
179
+ used.add(constructor);
180
+ }
181
+
182
+ // Recurse into nested specs (e.g., template.spec.containers)
183
+ if (typeof value === "object" && !Array.isArray(value)) {
184
+ this.collectNestedConstructors(value as Record<string, unknown>, used, parentType);
185
+ }
186
+ }
187
+
188
+ // Recurse into spec if present
189
+ if (props.spec && typeof props.spec === "object" && !Array.isArray(props.spec)) {
190
+ this.collectNestedConstructors(props.spec as Record<string, unknown>, used, parentType);
191
+ }
192
+ }
193
+
194
+ private emitProps(props: Record<string, unknown>, depth: number, parentType: string): string {
195
+ const indent = " ".repeat(depth);
196
+ const innerIndent = " ".repeat(depth + 1);
197
+ const entries: string[] = [];
198
+
199
+ for (const [key, value] of Object.entries(props)) {
200
+ if (value === undefined || value === null) continue;
201
+ const emitted = this.emitValue(key, value, depth + 1, parentType);
202
+ entries.push(`${innerIndent}${key}: ${emitted},`);
203
+ }
204
+
205
+ if (entries.length === 0) return "{}";
206
+ return `{\n${entries.join("\n")}\n${indent}}`;
207
+ }
208
+
209
+ private emitValue(key: string, value: unknown, depth: number, parentType: string): string {
210
+ if (value === null || value === undefined) return "undefined";
211
+
212
+ // Special case: "ports" on Service → ServicePort constructor
213
+ if (key === "ports" && SERVICE_PORT_TYPES.has(parentType) && Array.isArray(value)) {
214
+ return this.emitConstructorArray("ServicePort", value, depth, parentType);
215
+ }
216
+
217
+ // Check if this key maps to a property constructor
218
+ const constructor = PROPERTY_CONSTRUCTORS[key];
219
+ if (constructor) {
220
+ // Array of constructors (containers, volumes, ports, env, etc.)
221
+ if (Array.isArray(value)) {
222
+ return this.emitConstructorArray(constructor, value, depth, parentType);
223
+ }
224
+ // Single constructor (securityContext, resources, selector, template, strategy, etc.)
225
+ if (typeof value === "object") {
226
+ const propsStr = this.emitProps(value as Record<string, unknown>, depth, parentType);
227
+ return `new ${constructor}(${propsStr})`;
228
+ }
229
+ }
230
+
231
+ return this.emitLiteral(value, depth, parentType);
232
+ }
233
+
234
+ private emitConstructorArray(
235
+ constructor: string,
236
+ items: unknown[],
237
+ depth: number,
238
+ parentType: string,
239
+ ): string {
240
+ const indent = " ".repeat(depth);
241
+ const innerIndent = " ".repeat(depth + 1);
242
+
243
+ const emitted = items.map((item) => {
244
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
245
+ const propsStr = this.emitProps(item as Record<string, unknown>, depth + 1, parentType);
246
+ return `new ${constructor}(${propsStr})`;
247
+ }
248
+ return JSON.stringify(item);
249
+ });
250
+
251
+ if (emitted.length === 1 && emitted[0].length < 60) {
252
+ return `[${emitted[0]}]`;
253
+ }
254
+ return `[\n${emitted.map((e) => `${innerIndent}${e},`).join("\n")}\n${indent}]`;
255
+ }
256
+
257
+ private emitLiteral(value: unknown, depth: number, parentType: string): string {
258
+ if (value === null || value === undefined) return "undefined";
259
+ if (typeof value === "string") return JSON.stringify(value);
260
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
261
+
262
+ if (Array.isArray(value)) {
263
+ if (value.length === 0) return "[]";
264
+ const items = value.map((item) => this.emitLiteral(item, depth + 1, parentType));
265
+ const oneLine = `[${items.join(", ")}]`;
266
+ if (oneLine.length < 80) return oneLine;
267
+ const indent = " ".repeat(depth);
268
+ const innerIndent = " ".repeat(depth + 1);
269
+ return `[\n${items.map((i) => `${innerIndent}${i},`).join("\n")}\n${indent}]`;
270
+ }
271
+
272
+ if (typeof value === "object") {
273
+ const entries = Object.entries(value as Record<string, unknown>);
274
+ if (entries.length === 0) return "{}";
275
+ const indent = " ".repeat(depth);
276
+ const innerIndent = " ".repeat(depth + 1);
277
+ const items = entries.map(
278
+ ([k, v]) => `${innerIndent}${k}: ${this.emitLiteral(v, depth + 1, parentType)},`,
279
+ );
280
+ return `{\n${items.join("\n")}\n${indent}}`;
281
+ }
282
+
283
+ return String(value);
284
+ }
285
+ }
@@ -0,0 +1,156 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { K8sParser } from "./parser";
3
+
4
+ const parser = new K8sParser();
5
+
6
+ describe("K8sParser", () => {
7
+ test("empty YAML returns empty resources", () => {
8
+ const ir = parser.parse("");
9
+ expect(ir.resources).toEqual([]);
10
+ expect(ir.parameters).toEqual([]);
11
+ });
12
+
13
+ test("single Deployment parses correctly", () => {
14
+ const yaml = `
15
+ apiVersion: apps/v1
16
+ kind: Deployment
17
+ metadata:
18
+ name: my-app
19
+ spec:
20
+ replicas: 2
21
+ `;
22
+ const ir = parser.parse(yaml);
23
+ expect(ir.resources.length).toBe(1);
24
+ const r = ir.resources[0];
25
+ expect(r.type).toBe("K8s::Apps::Deployment");
26
+ expect(r.logicalId).toBe("deploymentMyApp");
27
+ expect(r.properties.spec).toEqual({ replicas: 2 });
28
+ });
29
+
30
+ test("multi-doc YAML produces multiple resources", () => {
31
+ const yaml = `
32
+ apiVersion: apps/v1
33
+ kind: Deployment
34
+ metadata:
35
+ name: app
36
+ ---
37
+ apiVersion: v1
38
+ kind: Service
39
+ metadata:
40
+ name: svc
41
+ `;
42
+ const ir = parser.parse(yaml);
43
+ expect(ir.resources.length).toBe(2);
44
+ });
45
+
46
+ test("apiVersion: apps/v1, kind: Deployment → K8s::Apps::Deployment", () => {
47
+ const yaml = `
48
+ apiVersion: apps/v1
49
+ kind: Deployment
50
+ metadata:
51
+ name: test
52
+ `;
53
+ const ir = parser.parse(yaml);
54
+ expect(ir.resources[0].type).toBe("K8s::Apps::Deployment");
55
+ });
56
+
57
+ test("apiVersion: v1, kind: Service → K8s::Core::Service", () => {
58
+ const yaml = `
59
+ apiVersion: v1
60
+ kind: Service
61
+ metadata:
62
+ name: test
63
+ `;
64
+ const ir = parser.parse(yaml);
65
+ expect(ir.resources[0].type).toBe("K8s::Core::Service");
66
+ });
67
+
68
+ test("v1/ConfigMap maps correctly", () => {
69
+ const yaml = `
70
+ apiVersion: v1
71
+ kind: ConfigMap
72
+ metadata:
73
+ name: config
74
+ data:
75
+ key: value
76
+ `;
77
+ const ir = parser.parse(yaml);
78
+ expect(ir.resources[0].type).toBe("K8s::Core::ConfigMap");
79
+ expect(ir.resources[0].properties.data).toEqual({ key: "value" });
80
+ });
81
+
82
+ test("unknown apiVersion/kind falls back to constructed type name", () => {
83
+ const yaml = `
84
+ apiVersion: custom.io/v1
85
+ kind: Widget
86
+ metadata:
87
+ name: w
88
+ `;
89
+ const ir = parser.parse(yaml);
90
+ expect(ir.resources[0].type).toMatch(/^K8s::\w+::Widget$/);
91
+ });
92
+
93
+ test("metadata.name extracted as logical ID component", () => {
94
+ const yaml = `
95
+ apiVersion: v1
96
+ kind: Pod
97
+ metadata:
98
+ name: my-pod
99
+ `;
100
+ const ir = parser.parse(yaml);
101
+ expect(ir.resources[0].logicalId).toBe("podMyPod");
102
+ expect(ir.resources[0].metadata!.originalName).toBe("my-pod");
103
+ });
104
+
105
+ test("metadata includes apiVersion and kind", () => {
106
+ const yaml = `
107
+ apiVersion: batch/v1
108
+ kind: Job
109
+ metadata:
110
+ name: my-job
111
+ `;
112
+ const ir = parser.parse(yaml);
113
+ expect(ir.resources[0].metadata!.apiVersion).toBe("batch/v1");
114
+ expect(ir.resources[0].metadata!.kind).toBe("Job");
115
+ });
116
+
117
+ test("properties exclude apiVersion and kind", () => {
118
+ const yaml = `
119
+ apiVersion: v1
120
+ kind: Pod
121
+ metadata:
122
+ name: test
123
+ spec:
124
+ containers: []
125
+ `;
126
+ const ir = parser.parse(yaml);
127
+ const props = ir.resources[0].properties;
128
+ expect(props.apiVersion).toBeUndefined();
129
+ expect(props.kind).toBeUndefined();
130
+ expect(props.metadata).toBeDefined();
131
+ expect(props.spec).toBeDefined();
132
+ });
133
+
134
+ test("parameters are empty (K8s has no parameters)", () => {
135
+ const yaml = `
136
+ apiVersion: v1
137
+ kind: Pod
138
+ metadata:
139
+ name: test
140
+ `;
141
+ const ir = parser.parse(yaml);
142
+ expect(ir.parameters).toEqual([]);
143
+ });
144
+
145
+ test("tracks namespaces in metadata", () => {
146
+ const yaml = `
147
+ apiVersion: v1
148
+ kind: Pod
149
+ metadata:
150
+ name: test
151
+ namespace: production
152
+ `;
153
+ const ir = parser.parse(yaml);
154
+ expect(ir.metadata?.namespaces).toContain("production");
155
+ });
156
+ });