@intentius/chant-lexicon-k8s 0.41.19 → 0.42.1

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 (276) hide show
  1. package/dist/components/builders.d.ts +4 -0
  2. package/dist/components/builders.d.ts.map +1 -1
  3. package/dist/components/capability-plugin.d.ts +1 -1
  4. package/dist/components/capability-plugin.d.ts.map +1 -1
  5. package/dist/components/index.d.ts +1 -0
  6. package/dist/components/index.d.ts.map +1 -1
  7. package/dist/components/kustomize-apply.d.ts +48 -0
  8. package/dist/components/kustomize-apply.d.ts.map +1 -0
  9. package/dist/deep-observe-hooks.d.ts +8 -0
  10. package/dist/deep-observe-hooks.d.ts.map +1 -1
  11. package/dist/describe-resources.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/integrity.json +2 -2
  15. package/dist/manifest.json +1 -1
  16. package/dist/okf/index.md +250 -0
  17. package/dist/okf/rules/ARGO001.md +16 -0
  18. package/dist/okf/rules/ARGO002.md +16 -0
  19. package/dist/okf/rules/ARGO003.md +15 -0
  20. package/dist/okf/rules/ARGO004.md +17 -0
  21. package/dist/okf/rules/ARGO005.md +15 -0
  22. package/dist/okf/rules/WK8001.md +16 -0
  23. package/dist/okf/rules/WK8002.md +20 -0
  24. package/dist/okf/rules/WK8003.md +19 -0
  25. package/dist/okf/rules/WK8005.md +11 -0
  26. package/dist/okf/rules/WK8006.md +11 -0
  27. package/dist/okf/rules/WK8041.md +11 -0
  28. package/dist/okf/rules/WK8042.md +11 -0
  29. package/dist/okf/rules/WK8101.md +11 -0
  30. package/dist/okf/rules/WK8102.md +11 -0
  31. package/dist/okf/rules/WK8103.md +11 -0
  32. package/dist/okf/rules/WK8104.md +15 -0
  33. package/dist/okf/rules/WK8105.md +11 -0
  34. package/dist/okf/rules/WK8201.md +11 -0
  35. package/dist/okf/rules/WK8202.md +11 -0
  36. package/dist/okf/rules/WK8203.md +11 -0
  37. package/dist/okf/rules/WK8204.md +11 -0
  38. package/dist/okf/rules/WK8205.md +11 -0
  39. package/dist/okf/rules/WK8207.md +11 -0
  40. package/dist/okf/rules/WK8208.md +11 -0
  41. package/dist/okf/rules/WK8209.md +11 -0
  42. package/dist/okf/rules/WK8301.md +16 -0
  43. package/dist/okf/rules/WK8302.md +11 -0
  44. package/dist/okf/rules/WK8303.md +15 -0
  45. package/dist/okf/rules/WK8304.md +15 -0
  46. package/dist/okf/rules/WK8305.md +15 -0
  47. package/dist/okf/rules/WK8306.md +11 -0
  48. package/dist/okf/rules/WK8401.md +15 -0
  49. package/dist/okf/rules/WK8402.md +15 -0
  50. package/dist/okf/rules/WK8403.md +15 -0
  51. package/dist/okf/types/APIGroup.md +16 -0
  52. package/dist/okf/types/APIGroupList.md +13 -0
  53. package/dist/okf/types/APIResourceList.md +14 -0
  54. package/dist/okf/types/APIService.md +14 -0
  55. package/dist/okf/types/APIServiceList.md +14 -0
  56. package/dist/okf/types/APIVersions.md +14 -0
  57. package/dist/okf/types/Alert.md +14 -0
  58. package/dist/okf/types/AppProject.md +19 -0
  59. package/dist/okf/types/Application.md +23 -0
  60. package/dist/okf/types/ApplicationSet.md +18 -0
  61. package/dist/okf/types/Backup.md +14 -0
  62. package/dist/okf/types/BatchJob.md +14 -0
  63. package/dist/okf/types/Binding.md +14 -0
  64. package/dist/okf/types/Bucket.md +14 -0
  65. package/dist/okf/types/CM.md +16 -0
  66. package/dist/okf/types/CSIDriver.md +14 -0
  67. package/dist/okf/types/CSIDriverList.md +14 -0
  68. package/dist/okf/types/CSINode.md +14 -0
  69. package/dist/okf/types/CSINodeList.md +14 -0
  70. package/dist/okf/types/CSIStorageCapacity.md +17 -0
  71. package/dist/okf/types/CSIStorageCapacityList.md +14 -0
  72. package/dist/okf/types/Certificate.md +18 -0
  73. package/dist/okf/types/CertificateRequest.md +14 -0
  74. package/dist/okf/types/CertificateSigningRequest.md +14 -0
  75. package/dist/okf/types/CertificateSigningRequestList.md +14 -0
  76. package/dist/okf/types/Challenge.md +14 -0
  77. package/dist/okf/types/Cluster.md +14 -0
  78. package/dist/okf/types/ClusterIssuer.md +14 -0
  79. package/dist/okf/types/ClusterRole.md +15 -0
  80. package/dist/okf/types/ClusterRoleBinding.md +15 -0
  81. package/dist/okf/types/ClusterRoleBindingList.md +14 -0
  82. package/dist/okf/types/ClusterRoleList.md +14 -0
  83. package/dist/okf/types/ClusterTrustBundle.md +14 -0
  84. package/dist/okf/types/ClusterTrustBundleList.md +14 -0
  85. package/dist/okf/types/ComponentStatus.md +14 -0
  86. package/dist/okf/types/ComponentStatusList.md +14 -0
  87. package/dist/okf/types/ConfigMapList.md +14 -0
  88. package/dist/okf/types/ControllerRevision.md +15 -0
  89. package/dist/okf/types/ControllerRevisionList.md +14 -0
  90. package/dist/okf/types/CoreEventList.md +14 -0
  91. package/dist/okf/types/CrdbCluster.md +14 -0
  92. package/dist/okf/types/CronJob.md +20 -0
  93. package/dist/okf/types/CronJobList.md +14 -0
  94. package/dist/okf/types/CustomResourceDefinition.md +14 -0
  95. package/dist/okf/types/CustomResourceDefinitionList.md +14 -0
  96. package/dist/okf/types/DaemonSet.md +19 -0
  97. package/dist/okf/types/DaemonSetList.md +14 -0
  98. package/dist/okf/types/DeleteOptions.md +18 -0
  99. package/dist/okf/types/Deploy.md +14 -0
  100. package/dist/okf/types/DeploymentList.md +14 -0
  101. package/dist/okf/types/DeviceClass.md +14 -0
  102. package/dist/okf/types/DeviceClassList.md +14 -0
  103. package/dist/okf/types/DeviceTaintRule.md +14 -0
  104. package/dist/okf/types/DeviceTaintRuleList.md +14 -0
  105. package/dist/okf/types/EndpointSlice.md +16 -0
  106. package/dist/okf/types/EndpointSliceList.md +14 -0
  107. package/dist/okf/types/Endpoints.md +14 -0
  108. package/dist/okf/types/EndpointsList.md +14 -0
  109. package/dist/okf/types/Event.md +27 -0
  110. package/dist/okf/types/EventsEvent.md +27 -0
  111. package/dist/okf/types/EventsEventList.md +14 -0
  112. package/dist/okf/types/Eviction.md +14 -0
  113. package/dist/okf/types/FlowSchema.md +14 -0
  114. package/dist/okf/types/FlowSchemaList.md +14 -0
  115. package/dist/okf/types/FluxInstance.md +14 -0
  116. package/dist/okf/types/FluxReport.md +14 -0
  117. package/dist/okf/types/GRPCRoute.md +14 -0
  118. package/dist/okf/types/Gateway.md +14 -0
  119. package/dist/okf/types/GatewayClass.md +14 -0
  120. package/dist/okf/types/GitRepository.md +14 -0
  121. package/dist/okf/types/HTTPRoute.md +14 -0
  122. package/dist/okf/types/HelmChart.md +14 -0
  123. package/dist/okf/types/HelmRelease.md +14 -0
  124. package/dist/okf/types/HelmRepository.md +14 -0
  125. package/dist/okf/types/HorizontalPodAutoscaler.md +14 -0
  126. package/dist/okf/types/HorizontalPodAutoscalerList.md +14 -0
  127. package/dist/okf/types/IPAddress.md +14 -0
  128. package/dist/okf/types/IPAddressList.md +14 -0
  129. package/dist/okf/types/ImagePolicy.md +14 -0
  130. package/dist/okf/types/ImageRepository.md +14 -0
  131. package/dist/okf/types/ImageUpdateAutomation.md +14 -0
  132. package/dist/okf/types/InfisicalDynamicSecret.md +14 -0
  133. package/dist/okf/types/InfisicalPushSecret.md +14 -0
  134. package/dist/okf/types/InfisicalSecret.md +14 -0
  135. package/dist/okf/types/Ing.md +14 -0
  136. package/dist/okf/types/IngressClass.md +14 -0
  137. package/dist/okf/types/IngressClassList.md +14 -0
  138. package/dist/okf/types/IngressList.md +14 -0
  139. package/dist/okf/types/IngressRoute.md +14 -0
  140. package/dist/okf/types/IngressRouteTCP.md +14 -0
  141. package/dist/okf/types/IngressRouteUDP.md +14 -0
  142. package/dist/okf/types/Issuer.md +14 -0
  143. package/dist/okf/types/JobList.md +14 -0
  144. package/dist/okf/types/Kustomization.md +14 -0
  145. package/dist/okf/types/Lease.md +14 -0
  146. package/dist/okf/types/LeaseCandidate.md +14 -0
  147. package/dist/okf/types/LeaseCandidateList.md +14 -0
  148. package/dist/okf/types/LeaseList.md +14 -0
  149. package/dist/okf/types/LimitRange.md +14 -0
  150. package/dist/okf/types/LimitRangeList.md +14 -0
  151. package/dist/okf/types/LocalSubjectAccessReview.md +14 -0
  152. package/dist/okf/types/MicroVM.md +14 -0
  153. package/dist/okf/types/MicroVMClass.md +14 -0
  154. package/dist/okf/types/MicroVMImage.md +14 -0
  155. package/dist/okf/types/MicroVMNetwork.md +14 -0
  156. package/dist/okf/types/MicroVMReplicaSet.md +14 -0
  157. package/dist/okf/types/Middleware.md +14 -0
  158. package/dist/okf/types/MiddlewareTCP.md +14 -0
  159. package/dist/okf/types/MutatingAdmissionPolicy.md +14 -0
  160. package/dist/okf/types/MutatingAdmissionPolicyBinding.md +14 -0
  161. package/dist/okf/types/MutatingAdmissionPolicyBindingList.md +14 -0
  162. package/dist/okf/types/MutatingAdmissionPolicyList.md +14 -0
  163. package/dist/okf/types/MutatingWebhookConfiguration.md +14 -0
  164. package/dist/okf/types/MutatingWebhookConfigurationList.md +14 -0
  165. package/dist/okf/types/Namespace.md +18 -0
  166. package/dist/okf/types/NamespaceList.md +14 -0
  167. package/dist/okf/types/NetPol.md +14 -0
  168. package/dist/okf/types/NetworkPolicyList.md +14 -0
  169. package/dist/okf/types/Node.md +21 -0
  170. package/dist/okf/types/NodeList.md +14 -0
  171. package/dist/okf/types/OCIRepository.md +14 -0
  172. package/dist/okf/types/ObjectStore.md +14 -0
  173. package/dist/okf/types/Order.md +14 -0
  174. package/dist/okf/types/PDB.md +18 -0
  175. package/dist/okf/types/PersistentVolume.md +14 -0
  176. package/dist/okf/types/PersistentVolumeClaim.md +14 -0
  177. package/dist/okf/types/PersistentVolumeClaimList.md +14 -0
  178. package/dist/okf/types/PersistentVolumeList.md +14 -0
  179. package/dist/okf/types/Pod.md +18 -0
  180. package/dist/okf/types/PodCertificateRequest.md +14 -0
  181. package/dist/okf/types/PodCertificateRequestList.md +14 -0
  182. package/dist/okf/types/PodDisruptionBudgetList.md +14 -0
  183. package/dist/okf/types/PodList.md +14 -0
  184. package/dist/okf/types/PodTemplate.md +14 -0
  185. package/dist/okf/types/PodTemplateList.md +14 -0
  186. package/dist/okf/types/Pooler.md +14 -0
  187. package/dist/okf/types/PriorityClass.md +17 -0
  188. package/dist/okf/types/PriorityClassList.md +14 -0
  189. package/dist/okf/types/PriorityLevelConfiguration.md +14 -0
  190. package/dist/okf/types/PriorityLevelConfigurationList.md +14 -0
  191. package/dist/okf/types/PrometheusRule.md +14 -0
  192. package/dist/okf/types/Provider.md +14 -0
  193. package/dist/okf/types/RayCluster.md +20 -0
  194. package/dist/okf/types/RayJob.md +14 -0
  195. package/dist/okf/types/RayService.md +14 -0
  196. package/dist/okf/types/Receiver.md +14 -0
  197. package/dist/okf/types/ReferenceGrant.md +14 -0
  198. package/dist/okf/types/ReplicaSet.md +19 -0
  199. package/dist/okf/types/ReplicaSetList.md +14 -0
  200. package/dist/okf/types/ReplicationController.md +14 -0
  201. package/dist/okf/types/ReplicationControllerList.md +14 -0
  202. package/dist/okf/types/ResourceClaim.md +14 -0
  203. package/dist/okf/types/ResourceClaimList.md +14 -0
  204. package/dist/okf/types/ResourceClaimTemplate.md +14 -0
  205. package/dist/okf/types/ResourceClaimTemplateList.md +14 -0
  206. package/dist/okf/types/ResourceQuota.md +14 -0
  207. package/dist/okf/types/ResourceQuotaList.md +14 -0
  208. package/dist/okf/types/ResourceSet.md +14 -0
  209. package/dist/okf/types/ResourceSetInputProvider.md +14 -0
  210. package/dist/okf/types/ResourceSlice.md +14 -0
  211. package/dist/okf/types/ResourceSliceList.md +14 -0
  212. package/dist/okf/types/Role.md +14 -0
  213. package/dist/okf/types/RoleBinding.md +15 -0
  214. package/dist/okf/types/RoleBindingList.md +14 -0
  215. package/dist/okf/types/RoleList.md +14 -0
  216. package/dist/okf/types/RuntimeClass.md +16 -0
  217. package/dist/okf/types/RuntimeClassList.md +14 -0
  218. package/dist/okf/types/SA.md +16 -0
  219. package/dist/okf/types/Scale.md +14 -0
  220. package/dist/okf/types/ScheduledBackup.md +14 -0
  221. package/dist/okf/types/Sec.md +17 -0
  222. package/dist/okf/types/SecretList.md +14 -0
  223. package/dist/okf/types/SelfSubjectAccessReview.md +14 -0
  224. package/dist/okf/types/SelfSubjectReview.md +13 -0
  225. package/dist/okf/types/SelfSubjectRulesReview.md +14 -0
  226. package/dist/okf/types/ServersTransport.md +14 -0
  227. package/dist/okf/types/ServersTransportTCP.md +14 -0
  228. package/dist/okf/types/Service.md +20 -0
  229. package/dist/okf/types/ServiceAccountList.md +14 -0
  230. package/dist/okf/types/ServiceCIDR.md +14 -0
  231. package/dist/okf/types/ServiceCIDRList.md +14 -0
  232. package/dist/okf/types/ServiceList.md +14 -0
  233. package/dist/okf/types/ServiceMonitor.md +14 -0
  234. package/dist/okf/types/StatefulSet.md +19 -0
  235. package/dist/okf/types/StatefulSetList.md +14 -0
  236. package/dist/okf/types/Status.md +17 -0
  237. package/dist/okf/types/StorageClass.md +20 -0
  238. package/dist/okf/types/StorageClassList.md +14 -0
  239. package/dist/okf/types/StorageVersionMigration.md +14 -0
  240. package/dist/okf/types/StorageVersionMigrationList.md +14 -0
  241. package/dist/okf/types/SubjectAccessReview.md +14 -0
  242. package/dist/okf/types/TLSOption.md +14 -0
  243. package/dist/okf/types/TLSStore.md +14 -0
  244. package/dist/okf/types/TokenRequest.md +14 -0
  245. package/dist/okf/types/TokenReview.md +14 -0
  246. package/dist/okf/types/TraefikService.md +14 -0
  247. package/dist/okf/types/ValidatingAdmissionPolicy.md +14 -0
  248. package/dist/okf/types/ValidatingAdmissionPolicyBinding.md +14 -0
  249. package/dist/okf/types/ValidatingAdmissionPolicyBindingList.md +14 -0
  250. package/dist/okf/types/ValidatingAdmissionPolicyList.md +14 -0
  251. package/dist/okf/types/ValidatingWebhookConfiguration.md +14 -0
  252. package/dist/okf/types/ValidatingWebhookConfigurationList.md +14 -0
  253. package/dist/okf/types/VolumeAttachment.md +14 -0
  254. package/dist/okf/types/VolumeAttachmentList.md +14 -0
  255. package/dist/okf/types/VolumeAttributesClass.md +15 -0
  256. package/dist/okf/types/VolumeAttributesClassList.md +14 -0
  257. package/dist/okf/types/WatchEvent.md +14 -0
  258. package/dist/op/activities/kubectl.d.ts +15 -1
  259. package/dist/op/activities/kubectl.d.ts.map +1 -1
  260. package/dist/op/activities/wait-for-ready.d.ts +16 -6
  261. package/dist/op/activities/wait-for-ready.d.ts.map +1 -1
  262. package/package.json +2 -2
  263. package/src/components/builders.ts +5 -0
  264. package/src/components/capability-plugin.ts +3 -2
  265. package/src/components/index.ts +7 -0
  266. package/src/components/kustomize-apply.test.ts +98 -0
  267. package/src/components/kustomize-apply.ts +133 -0
  268. package/src/deep-observe-hooks.ts +60 -0
  269. package/src/deep-observe.test.ts +114 -0
  270. package/src/describe-resources.test.ts +159 -0
  271. package/src/describe-resources.ts +142 -2
  272. package/src/index.ts +1 -0
  273. package/src/op/activities/kubectl.test.ts +22 -0
  274. package/src/op/activities/kubectl.ts +15 -2
  275. package/src/op/activities/wait-for-ready.test.ts +27 -0
  276. package/src/op/activities/wait-for-ready.ts +84 -6
@@ -19,3 +19,10 @@ export {
19
19
  type KubectlApplyInput,
20
20
  type KubectlApplyOutcome,
21
21
  } from "./kubectl-apply";
22
+ export {
23
+ kustomizeApplyCapability,
24
+ createKustomizeApplyCapability,
25
+ renderCommand,
26
+ type KustomizeApplyInput,
27
+ type KustomizeApplyOutcome,
28
+ } from "./kustomize-apply";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `kustomize-apply` (#1548) — the render-then-apply leaf. What must hold:
3
+ * the capability registers under its kind with the lexicon's own version
4
+ * (#1505), the render command is exact and falls back to kubectl's vendored
5
+ * kustomize, the rendered documents reach the applier INLINE (no temp file)
6
+ * with the deploy-unit stack, and the rollback posture matches kubectl-apply
7
+ * (server-side apply has no native undo).
8
+ */
9
+ import { describe, test, expect } from "vitest";
10
+ import { createKustomizeApplyCapability, kustomizeApplyCapability, renderCommand, type KustomizeApplyInput } from "./kustomize-apply";
11
+ import { k8sCapabilityPlugin, K8S_VERB_FAMILIES } from "./capability-plugin";
12
+ import { isCapabilityPlugin } from "@intentius/chant/components/capability-plugin";
13
+ import type { DeployContext } from "@intentius/chant/components/capability";
14
+
15
+ const ctx = { env: "dev" } as DeployContext;
16
+
17
+ const RENDERED = `apiVersion: v1
18
+ kind: Namespace
19
+ metadata:
20
+ name: web
21
+ ---
22
+ apiVersion: apps/v1
23
+ kind: Deployment
24
+ metadata:
25
+ name: app
26
+ namespace: web
27
+ spec:
28
+ replicas: 1
29
+ `;
30
+
31
+ describe("kustomize-apply capability (#1548)", () => {
32
+ test("registers on the k8s plugin, in the apply family, at the package's own version", () => {
33
+ expect(isCapabilityPlugin(k8sCapabilityPlugin)).toBe(true);
34
+ expect(k8sCapabilityPlugin.capabilities().map((c) => c.kind)).toContain("kustomize-apply");
35
+ expect(K8S_VERB_FAMILIES.apply).toContain("kustomize-apply");
36
+ // #1505 — never a stale literal.
37
+ const { version } = JSON.parse(
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
+ require("node:fs").readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
40
+ ) as { version: string };
41
+ expect(k8sCapabilityPlugin.version).toBe(version);
42
+ });
43
+
44
+ test("rollback is needs-opt-out — a server-side apply keeps no previous state", () => {
45
+ expect(kustomizeApplyCapability.rollbackPolicy).toBe("needs-opt-out");
46
+ });
47
+
48
+ test("renders with kustomize build and hands the parsed documents to the applier inline", async () => {
49
+ const commands: string[] = [];
50
+ const applierArgs: unknown[] = [];
51
+ const cap = createKustomizeApplyCapability(
52
+ async (command) => {
53
+ commands.push(command);
54
+ return { stdout: RENDERED };
55
+ },
56
+ async (args) => {
57
+ applierArgs.push(args);
58
+ return { fieldManager: "chant:web", applied: [], pruned: [] };
59
+ },
60
+ );
61
+
62
+ await cap.run(ctx, { dir: "overlays/dev", stack: "web", delete: "owned-only" } as KustomizeApplyInput as never);
63
+
64
+ expect(commands).toEqual(["kustomize build 'overlays/dev'"]);
65
+ expect(applierArgs).toHaveLength(1);
66
+ const args = applierArgs[0] as { manifest: string; documents: unknown[]; environment: string; stack: string; deleteMode: string };
67
+ expect(args.manifest).toBe("kustomize:overlays/dev"); // a label, not a path
68
+ expect(args.documents).toHaveLength(2);
69
+ expect((args.documents[1] as { kind: string }).kind).toBe("Deployment");
70
+ expect(args.environment).toBe("dev");
71
+ expect(args.stack).toBe("web");
72
+ expect(args.deleteMode).toBe("owned-only");
73
+ });
74
+
75
+ test("falls back to kubectl's vendored kustomize when the binary is missing", async () => {
76
+ const commands: string[] = [];
77
+ const cap = createKustomizeApplyCapability(
78
+ async (command) => {
79
+ commands.push(command);
80
+ if (command.startsWith("kustomize ")) throw new Error("spawn kustomize ENOENT");
81
+ return { stdout: RENDERED };
82
+ },
83
+ async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
84
+ );
85
+ await cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never);
86
+ expect(commands).toEqual([renderCommand("overlays/dev", "kustomize"), renderCommand("overlays/dev", "kubectl")]);
87
+ });
88
+
89
+ test("a render failure that is NOT a missing binary propagates — a broken overlay is not silently retried", async () => {
90
+ const cap = createKustomizeApplyCapability(
91
+ async () => {
92
+ throw new Error("accumulating resources: missing base ../common");
93
+ },
94
+ async () => ({ fieldManager: "chant", applied: [], pruned: [] }),
95
+ );
96
+ await expect(cap.run(ctx, { dir: "overlays/dev" } as KustomizeApplyInput as never)).rejects.toThrow("missing base");
97
+ });
98
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `kustomize-apply` — the kustomize leaf for the component model (#1548).
3
+ *
4
+ * Kustomize is a RENDERER, not a substrate: `kustomize build <dir>` emits
5
+ * exactly the Kubernetes manifests the existing apply pipeline was built for.
6
+ * So this capability is a render step in front of `kubectl-apply`'s machinery
7
+ * — the rendered documents go straight to the same server-side applier
8
+ * (inline, no temp file), which stamps the deploy-unit stack (#1539), prunes
9
+ * marker-scoped, retakes chant's own field conflicts (#1541/#1542), and is
10
+ * observed by the same `describeStackStatus` label sweep. An estate that
11
+ * keeps its overlay tree gets components-status honesty without adopting a
12
+ * single typed manifest.
13
+ *
14
+ * `stack` doubles as the ownership identity and the deploy unit, exactly as
15
+ * `kubectl-apply` — one name, both jobs.
16
+ *
17
+ * Renderer resolution: `kustomize build`, falling back to
18
+ * `kubectl kustomize` (the same renderer vendored into kubectl) when the
19
+ * standalone binary is absent. Both run through an injectable runner so tests
20
+ * assert the exact command without either binary installed. The 64MiB
21
+ * maxBuffer mirrors helm-upgrade's — a big overlay renders megabytes.
22
+ *
23
+ * The #1074 boundary applies as it does to kubectl-apply: nothing exported
24
+ * from the lexicon entry point may statically import the API-client chain, so
25
+ * the applier is reached by dynamic import inside `run()` and its argument
26
+ * shape is a structural mirror.
27
+ */
28
+ import { exec } from "node:child_process";
29
+ import { promisify } from "node:util";
30
+ import { loadAll } from "js-yaml";
31
+ import type { Capability, DeployContext } from "@intentius/chant/components/capability";
32
+
33
+ const execAsync = promisify(exec);
34
+
35
+ /** Injectable shell runner — the helm-upgrade seam, same buffer bound. */
36
+ export type KustomizeRunner = (command: string) => Promise<{ stdout: string }>;
37
+ const defaultRunner: KustomizeRunner = (command) => execAsync(command, { maxBuffer: 64 * 1024 * 1024 });
38
+
39
+ /** Single-quote shell escaping, as helm-upgrade quotes its argv. */
40
+ function q(v: string): string {
41
+ return `'${v.replace(/'/g, "'\\''")}'`;
42
+ }
43
+
44
+ /** Structural mirrors of the activity module's shapes (see the module doc). */
45
+ interface AppliedRef {
46
+ apiVersion: string;
47
+ kind: string;
48
+ name: string;
49
+ namespace?: string;
50
+ }
51
+
52
+ export interface KustomizeApplyOutcome {
53
+ fieldManager: string;
54
+ applied: AppliedRef[];
55
+ pruned: AppliedRef[];
56
+ }
57
+
58
+ interface ApplierArgs {
59
+ manifest: string;
60
+ documents?: Array<Record<string, unknown>>;
61
+ environment?: string;
62
+ stack?: string;
63
+ context?: string;
64
+ deleteMode?: "never" | "owned-only" | "gated";
65
+ }
66
+
67
+ type Applier = (args: ApplierArgs) => Promise<KustomizeApplyOutcome>;
68
+
69
+ export interface KustomizeApplyInput {
70
+ /** The kustomization directory (holds `kustomization.yaml`) — an overlay or a base. */
71
+ dir: string;
72
+ /**
73
+ * The deploy unit / ownership stack — same double duty as kubectl-apply's.
74
+ * Omitted derives from the project's `ownership.stack`, but then the unit is
75
+ * invisible to `components status --live`. Name it.
76
+ */
77
+ stack?: string;
78
+ /** kubectl context. Omitted resolves `k8s.profiles.<ctx.env>.context`. */
79
+ context?: string;
80
+ /** Same delete vocabulary as kubectl-apply; default `never`. */
81
+ delete?: "never" | "owned-only";
82
+ }
83
+
84
+ /** The exact render command, standalone binary first. Pure; exported for tests. */
85
+ export function renderCommand(dir: string, tool: "kustomize" | "kubectl" = "kustomize"): string {
86
+ return tool === "kustomize" ? `kustomize build ${q(dir)}` : `kubectl kustomize ${q(dir)}`;
87
+ }
88
+
89
+ /** Render the overlay, falling back to kubectl's vendored kustomize when the
90
+ * standalone binary is missing (ENOENT/127), and parse the emitted YAML. */
91
+ async function renderDocuments(dir: string, run: KustomizeRunner): Promise<Array<Record<string, unknown>>> {
92
+ let stdout: string;
93
+ try {
94
+ ({ stdout } = await run(renderCommand(dir, "kustomize")));
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ if (!/ENOENT|not found|command not found|127/.test(message)) throw err;
98
+ ({ stdout } = await run(renderCommand(dir, "kubectl")));
99
+ }
100
+ const documents: Array<Record<string, unknown>> = [];
101
+ for (const doc of loadAll(stdout)) {
102
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) documents.push(doc as Record<string, unknown>);
103
+ }
104
+ return documents;
105
+ }
106
+
107
+ /** Factory with injectable renderer + applier, so tests assert the render
108
+ * command and the exact applier delegation without kustomize or a cluster. */
109
+ export function createKustomizeApplyCapability(
110
+ run: KustomizeRunner = defaultRunner,
111
+ apply?: Applier,
112
+ ): Capability<KustomizeApplyInput, KustomizeApplyOutcome> {
113
+ return {
114
+ kind: "kustomize-apply",
115
+ // Server-side apply keeps no previous object state, and kustomize adds no
116
+ // undo of its own — same posture as kubectl-apply.
117
+ rollbackPolicy: "needs-opt-out",
118
+ async run(ctx: DeployContext, input: KustomizeApplyInput): Promise<KustomizeApplyOutcome> {
119
+ const documents = await renderDocuments(input.dir, run);
120
+ const applier: Applier = apply ?? ((await import("../op/activities/kubectl")).applyManifest as unknown as Applier);
121
+ return applier({
122
+ manifest: `kustomize:${input.dir}`,
123
+ documents,
124
+ environment: ctx.env,
125
+ ...(input.stack !== undefined ? { stack: input.stack } : {}),
126
+ ...(input.context !== undefined ? { context: input.context } : {}),
127
+ ...(input.delete !== undefined ? { deleteMode: input.delete } : {}),
128
+ });
129
+ },
130
+ };
131
+ }
132
+
133
+ export const kustomizeApplyCapability = createKustomizeApplyCapability();
@@ -44,6 +44,7 @@
44
44
  import { createRequire } from "module";
45
45
  import type { DeepArrayElement, DeepNode, DeepNormalizationHooks } from "@intentius/chant/lexicon";
46
46
  import { K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS, k8sListMapOrderKey } from "@intentius/chant/managed-fields";
47
+ import { LABEL_OWNERSHIP_KEYS, OWNERSHIP_MANAGED_BY_VALUE } from "@intentius/chant/ownership";
47
48
 
48
49
  // This module ships as ESM (tsx strips types from src/ directly), where a bare
49
50
  // `require` is not defined — the same reason serializer.ts and the LSP modules
@@ -86,13 +87,64 @@ export const K8S_SERVICE_DEFAULTS: Record<string, Record<string, unknown>> = {
86
87
  "spec.template.spec.restartPolicy": "Always",
87
88
  "spec.template.spec.terminationGracePeriodSeconds": 30,
88
89
  "spec.template.spec.schedulerName": "default-scheduler",
90
+ // Widened on the CC canonical estate (#1214): a container declaring an
91
+ // httpGet probe, a named port, or nothing about termination gets exactly
92
+ // these from the API server. SSA attributes a server-defaulted field to no
93
+ // manager, so the managed-fields prune deliberately leaves them diffable
94
+ // (see ./deep-observe.ts) — this table is where they subtract.
95
+ "spec.template.spec.containers[].livenessProbe.failureThreshold": 3,
96
+ "spec.template.spec.containers[].livenessProbe.periodSeconds": 10,
97
+ "spec.template.spec.containers[].livenessProbe.successThreshold": 1,
98
+ "spec.template.spec.containers[].livenessProbe.timeoutSeconds": 1,
99
+ "spec.template.spec.containers[].livenessProbe.httpGet.scheme": "HTTP",
100
+ "spec.template.spec.containers[].readinessProbe.failureThreshold": 3,
101
+ "spec.template.spec.containers[].readinessProbe.periodSeconds": 10,
102
+ "spec.template.spec.containers[].readinessProbe.successThreshold": 1,
103
+ "spec.template.spec.containers[].readinessProbe.timeoutSeconds": 1,
104
+ "spec.template.spec.containers[].readinessProbe.httpGet.scheme": "HTTP",
105
+ "spec.template.spec.containers[].ports[].protocol": "TCP",
106
+ "spec.template.spec.containers[].terminationMessagePath": "/dev/termination-log",
107
+ "spec.template.spec.containers[].terminationMessagePolicy": "File",
108
+ "spec.template.spec.securityContext": {},
89
109
  },
90
110
  "K8s::Core::Service": {
91
111
  "spec.sessionAffinity": "None",
92
112
  "spec.type": "ClusterIP",
113
+ "spec.internalTrafficPolicy": "Cluster",
114
+ "spec.ipFamilyPolicy": "SingleStack",
115
+ "spec.ports[].protocol": "TCP",
93
116
  },
94
117
  };
95
118
 
119
+ /**
120
+ * Server-ASSIGNED fields — populated by the API server with a value that
121
+ * varies per object (an allocated ClusterIP, the resolved IP family list), so
122
+ * unlike {@link K8S_SERVICE_DEFAULTS} there is no fixed value to compare
123
+ * against. Counterpart-gated like the defaults table: a declared `clusterIP`
124
+ * is still compared, only the purely server-filled appearance is noise.
125
+ */
126
+ export const K8S_SERVER_ASSIGNED_PATTERNS: Record<string, ReadonlySet<string>> = {
127
+ "K8s::Core::Service": new Set(["spec.clusterIP", "spec.clusterIPs", "spec.ipFamilies"]),
128
+ };
129
+
130
+ /**
131
+ * chant's own ownership marker, as label paths (#1214 — the k8s edition of
132
+ * azure's #1213 rule). The build stamps `app.kubernetes.io/managed-by` +
133
+ * `chant.intentius.io/{stack,env}` into every manifest it emits, so a managed
134
+ * object always carries them live while the declared *source* the diff
135
+ * compares does not — chant reading its own signature back as drift. The
136
+ * managed-fields prune cannot subtract them: they are in the applied config,
137
+ * so chant's field manager owns them, and chant-owned is always diffable.
138
+ * Counterpart-gated at the call site; managed-by is additionally gated on the
139
+ * value `"chant"`, so `managed-by: helm` appearing out of band still surfaces.
140
+ */
141
+ const K8S_OWNERSHIP_LABEL_PATTERNS: ReadonlySet<string> = new Set([
142
+ `metadata.labels.${LABEL_OWNERSHIP_KEYS.stack}`,
143
+ `metadata.labels.${LABEL_OWNERSHIP_KEYS.env}`,
144
+ ]);
145
+
146
+ const K8S_MANAGED_BY_LABEL_PATTERN = `metadata.labels.${LABEL_OWNERSHIP_KEYS.managedBy}`;
147
+
96
148
  /** Stable JSON with sorted keys — the fallback ordering key for a set-like array without a natural identity field. */
97
149
  function canonicalJson(value: unknown): string {
98
150
  return (
@@ -206,6 +258,14 @@ export const k8sDeepNormalizationHooks: DeepNormalizationHooks = {
206
258
  if (K8S_OBJECT_ENVELOPE_PRUNE_PATTERNS.has(node.pattern)) return true;
207
259
 
208
260
  if (node.side !== "live" || node.counterpart !== "absent") return false;
261
+
262
+ // chant's own ownership marker is not drift (see the sets' docs).
263
+ if (K8S_OWNERSHIP_LABEL_PATTERNS.has(node.pattern)) return true;
264
+ if (node.pattern === K8S_MANAGED_BY_LABEL_PATTERN && node.value === OWNERSHIP_MANAGED_BY_VALUE) return true;
265
+
266
+ // A server-assigned value nobody declared is allocation, not drift.
267
+ if (K8S_SERVER_ASSIGNED_PATTERNS[node.entityType]?.has(node.pattern)) return true;
268
+
209
269
  const defaults = K8S_SERVICE_DEFAULTS[node.entityType];
210
270
  if (!defaults || !Object.prototype.hasOwnProperty.call(defaults, node.pattern)) return false;
211
271
  return canonicalJson(defaults[node.pattern]) === canonicalJson(node.value);
@@ -96,6 +96,120 @@ describe("k8sDeepNormalizationHooks — the static rules", () => {
96
96
  expect(declaredExplicit).toEqual({ spec: { strategy: strategyDefault, replicas: 3 } });
97
97
  });
98
98
 
99
+ test("chant's own stamped labels are not drift; a foreign managed-by and a user label still are", () => {
100
+ const live = {
101
+ metadata: {
102
+ name: "web",
103
+ labels: {
104
+ "app.kubernetes.io/managed-by": "chant",
105
+ "chant.intentius.io/stack": "cc-azure-canonical",
106
+ "chant.intentius.io/env": "local",
107
+ team: "platform",
108
+ },
109
+ },
110
+ };
111
+ const out = normalizeDeepProperties(live, {
112
+ entityType: "K8s::Apps::Deployment",
113
+ side: "live",
114
+ hooks: k8sDeepNormalizationHooks,
115
+ counterpartPaths: new Set(["metadata", "metadata.name"]),
116
+ });
117
+ expect(out).toEqual({ metadata: { name: "web", labels: { team: "platform" } } });
118
+
119
+ const foreign = normalizeDeepProperties(
120
+ { metadata: { name: "web", labels: { "app.kubernetes.io/managed-by": "helm" } } },
121
+ {
122
+ entityType: "K8s::Apps::Deployment",
123
+ side: "live",
124
+ hooks: k8sDeepNormalizationHooks,
125
+ counterpartPaths: new Set(["metadata", "metadata.name"]),
126
+ },
127
+ );
128
+ expect(foreign).toEqual({ metadata: { name: "web", labels: { "app.kubernetes.io/managed-by": "helm" } } });
129
+ });
130
+
131
+ test("a server-assigned ClusterIP nobody declared subtracts; a declared one is compared", () => {
132
+ const undeclared = normalizeDeepProperties(
133
+ { spec: { clusterIP: "10.43.201.161", clusterIPs: ["10.43.201.161"], ipFamilies: ["IPv4"], selector: { app: "web" } } },
134
+ {
135
+ entityType: "K8s::Core::Service",
136
+ side: "live",
137
+ hooks: k8sDeepNormalizationHooks,
138
+ counterpartPaths: new Set(["spec", "spec.selector", "spec.selector.app"]),
139
+ },
140
+ );
141
+ expect(undeclared).toEqual({ spec: { selector: { app: "web" } } });
142
+
143
+ const declared = normalizeDeepProperties(
144
+ { spec: { clusterIP: "10.43.0.10" } },
145
+ {
146
+ entityType: "K8s::Core::Service",
147
+ side: "live",
148
+ hooks: k8sDeepNormalizationHooks,
149
+ counterpartPaths: new Set(["spec", "spec.clusterIP"]),
150
+ },
151
+ );
152
+ expect(declared).toEqual({ spec: { clusterIP: "10.43.0.10" } });
153
+ });
154
+
155
+ test("probe and termination defaults subtract inside a declared container", () => {
156
+ const out = normalizeDeepProperties(
157
+ {
158
+ spec: {
159
+ template: {
160
+ spec: {
161
+ securityContext: {},
162
+ containers: [
163
+ {
164
+ name: "api",
165
+ image: "example:1",
166
+ terminationMessagePath: "/dev/termination-log",
167
+ terminationMessagePolicy: "File",
168
+ livenessProbe: { httpGet: { path: "/", port: 8080, scheme: "HTTP" }, failureThreshold: 3, periodSeconds: 10, successThreshold: 1, timeoutSeconds: 1 },
169
+ ports: [{ containerPort: 8080, name: "http", protocol: "TCP" }],
170
+ },
171
+ ],
172
+ },
173
+ },
174
+ },
175
+ },
176
+ {
177
+ entityType: "K8s::Apps::Deployment",
178
+ side: "live",
179
+ hooks: k8sDeepNormalizationHooks,
180
+ counterpartPaths: new Set([
181
+ "spec", "spec.template", "spec.template.spec",
182
+ "spec.template.spec.containers", "spec.template.spec.containers[]",
183
+ "spec.template.spec.containers[].name", "spec.template.spec.containers[].image",
184
+ "spec.template.spec.containers[].livenessProbe",
185
+ "spec.template.spec.containers[].livenessProbe.httpGet",
186
+ "spec.template.spec.containers[].livenessProbe.httpGet.path",
187
+ "spec.template.spec.containers[].livenessProbe.httpGet.port",
188
+ "spec.template.spec.containers[].ports",
189
+ "spec.template.spec.containers[].ports[]",
190
+ "spec.template.spec.containers[].ports[].containerPort",
191
+ "spec.template.spec.containers[].ports[].name",
192
+ ]),
193
+ },
194
+ );
195
+ expect(out).toEqual({
196
+ spec: {
197
+ template: {
198
+ spec: {
199
+ containers: [
200
+ {
201
+ name: "api",
202
+ image: "example:1",
203
+ livenessProbe: { httpGet: { path: "/", port: 8080 } },
204
+ ports: [{ containerPort: 8080, name: "http" }],
205
+ },
206
+ ],
207
+ },
208
+ },
209
+ },
210
+ });
211
+ });
212
+
99
213
  test("a one-sided pass never subtracts defaults — the reader has no declared tree yet", () => {
100
214
  const out = normalizeDeepProperties(
101
215
  { spec: { strategy: { type: "RollingUpdate", rollingUpdate: { maxSurge: "25%", maxUnavailable: "25%" } } } },
@@ -573,6 +573,135 @@ describe("k8s describeResources", () => {
573
573
  expect(result.resources["prod/hand-made"]).toBeUndefined();
574
574
  });
575
575
 
576
+ test("a Flux-managed Deployment is attributed to its declared Kustomization by the controller's labels (#1549)", async () => {
577
+ // Flux stamps managed objects with name+namespace labels instead of
578
+ // ownerReferences, so the chain walk reaches nothing — the label
579
+ // channel resolves against the DECLARED CR, exactly. The workload also
580
+ // lives in a namespace the estate never declares an entity in: the
581
+ // Kustomization's own spec.targetNamespace is what puts it in scope.
582
+ const kustomization = {
583
+ apiVersion: "kustomize.toolkit.fluxcd.io/v1",
584
+ kind: "Kustomization",
585
+ metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
586
+ status: { conditions: [{ type: "Ready", status: "True" }] },
587
+ };
588
+ const managed = {
589
+ apiVersion: "apps/v1",
590
+ kind: "Deployment",
591
+ metadata: {
592
+ name: "web",
593
+ namespace: "prod",
594
+ uid: "dep-uid",
595
+ labels: { "kustomize.toolkit.fluxcd.io/name": "apps", "kustomize.toolkit.fluxcd.io/namespace": "flux-system" },
596
+ },
597
+ status: { readyReplicas: 1, replicas: 1 },
598
+ };
599
+ const cluster = fakeCluster({
600
+ objects: {
601
+ [objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
602
+ [objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
603
+ },
604
+ });
605
+
606
+ const result = await describeResources(
607
+ {
608
+ environment: "prod",
609
+ buildOutput: "",
610
+ entityNames: ["apps"],
611
+ entities: makeEntities([
612
+ {
613
+ name: "apps",
614
+ entityType: "K8s::Flux::Kustomization",
615
+ props: { metadata: { name: "apps", namespace: "flux-system" }, spec: { targetNamespace: "prod", sourceRef: { kind: "GitRepository", name: "infra" } } },
616
+ },
617
+ ]),
618
+ },
619
+ cluster.connector,
620
+ );
621
+
622
+ expect(result.resources["prod/web"]).toMatchObject({
623
+ type: "K8s::Apps::Deployment",
624
+ ownerChain: { root: "declared", entity: "apps" },
625
+ });
626
+ });
627
+
628
+ test("Argo's bare instance label claims only a UNIQUE declared Application (#1549)", async () => {
629
+ const app = (name: string) => ({
630
+ apiVersion: "argoproj.io/v1alpha1",
631
+ kind: "Application",
632
+ metadata: { name, namespace: "argocd", uid: `${name}-uid` },
633
+ status: { health: { status: "Healthy" }, sync: { status: "Synced" } },
634
+ });
635
+ const managed = {
636
+ apiVersion: "apps/v1",
637
+ kind: "Deployment",
638
+ metadata: { name: "web", namespace: "prod", uid: "dep-uid", labels: { "app.kubernetes.io/instance": "web" } },
639
+ };
640
+ const cluster = fakeCluster({
641
+ objects: {
642
+ [objectKey("argoproj.io/v1alpha1", "Application", "web", "argocd")]: app("web"),
643
+ [objectKey("apps/v1", "Deployment", "web", "prod")]: managed,
644
+ },
645
+ });
646
+
647
+ const result = await describeResources(
648
+ {
649
+ environment: "prod",
650
+ buildOutput: "",
651
+ entityNames: ["webApp"],
652
+ entities: makeEntities([
653
+ {
654
+ name: "webApp",
655
+ entityType: "K8s::Argo::Application",
656
+ props: { metadata: { name: "web", namespace: "argocd" }, spec: { destination: { namespace: "prod" } } },
657
+ },
658
+ ]),
659
+ },
660
+ cluster.connector,
661
+ );
662
+
663
+ expect(result.resources["prod/web"]).toMatchObject({
664
+ type: "K8s::Apps::Deployment",
665
+ ownerChain: { root: "declared", entity: "webApp" },
666
+ });
667
+ });
668
+
669
+ test("a generic instance label matching NO declared Application claims nothing — not an inventory (#1549)", async () => {
670
+ // `app.kubernetes.io/instance` is also what plain helm sets; on an
671
+ // estate declaring a Flux CR (so the widened sweep runs), a foreign
672
+ // helm-installed Deployment must not be attributed.
673
+ const kustomization = {
674
+ apiVersion: "kustomize.toolkit.fluxcd.io/v1",
675
+ kind: "Kustomization",
676
+ metadata: { name: "apps", namespace: "flux-system", uid: "ks-uid" },
677
+ };
678
+ const foreign = {
679
+ apiVersion: "apps/v1",
680
+ kind: "Deployment",
681
+ metadata: { name: "cert-manager", namespace: "flux-system", uid: "cm-uid", labels: { "app.kubernetes.io/instance": "cert-manager" } },
682
+ };
683
+ const cluster = fakeCluster({
684
+ objects: {
685
+ [objectKey("kustomize.toolkit.fluxcd.io/v1", "Kustomization", "apps", "flux-system")]: kustomization,
686
+ [objectKey("apps/v1", "Deployment", "cert-manager", "flux-system")]: foreign,
687
+ },
688
+ });
689
+
690
+ const result = await describeResources(
691
+ {
692
+ environment: "prod",
693
+ buildOutput: "",
694
+ entityNames: ["apps"],
695
+ entities: makeEntities([
696
+ { name: "apps", entityType: "K8s::Flux::Kustomization", props: { metadata: { name: "apps", namespace: "flux-system" } } },
697
+ ]),
698
+ },
699
+ cluster.connector,
700
+ );
701
+
702
+ expect(result.resources["flux-system/cert-manager"]).toBeUndefined();
703
+ });
704
+
576
705
  test("a core-only estate sweeps no extra groups — Pods stay the core representative", async () => {
577
706
  const svc = { apiVersion: "v1", kind: "Service", metadata: { name: "web", namespace: "prod", uid: "svc-uid" } };
578
707
  const cluster = fakeCluster({
@@ -880,6 +1009,36 @@ describe("statusFromObject — the most specific failing signal (#1397)", () =>
880
1009
  expect(statusFromObject({ status: { readyReplicas: 1, replicas: 3 } } as never)).toBe("PROGRESSING(1/3)");
881
1010
  expect(statusFromObject({} as never)).toBe("PRESENT");
882
1011
  });
1012
+
1013
+ test("a conditions-first CR reads its Ready condition, not PRESENT (#1549)", () => {
1014
+ // A wedged Flux Kustomization/HelmRelease has no phase and no replica
1015
+ // counts, so it fell straight to PRESENT — a wedged reconciler read
1016
+ // exactly like a healthy one.
1017
+ expect(
1018
+ statusFromObject({ status: { conditions: [{ type: "Ready", status: "False", reason: "BuildFailed" }] } } as never),
1019
+ ).toBe("BuildFailed");
1020
+ expect(
1021
+ statusFromObject({ status: { conditions: [{ type: "Ready", status: "False" }] } } as never),
1022
+ ).toBe("NOT-READY");
1023
+ expect(
1024
+ statusFromObject({ status: { conditions: [{ type: "Ready", status: "True", reason: "ReconciliationSucceeded" }] } } as never),
1025
+ ).toBe("READY");
1026
+ });
1027
+
1028
+ test("the Ready rung never changes a Pod or workload word — phase and replicas rank above it (#1549)", () => {
1029
+ expect(
1030
+ statusFromObject({ status: { phase: "Running", conditions: [{ type: "Ready", status: "False" }] } } as never),
1031
+ ).toBe("Running");
1032
+ expect(
1033
+ statusFromObject({ status: { readyReplicas: 1, replicas: 3, conditions: [{ type: "Ready", status: "False" }] } } as never),
1034
+ ).toBe("PROGRESSING(1/3)");
1035
+ });
1036
+
1037
+ test("an Argo Application reads health/sync (#1549)", () => {
1038
+ expect(statusFromObject({ status: { health: { status: "Degraded" }, sync: { status: "Synced" } } } as never)).toBe("Degraded");
1039
+ expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "OutOfSync" } } } as never)).toBe("OutOfSync");
1040
+ expect(statusFromObject({ status: { health: { status: "Healthy" }, sync: { status: "Synced" } } } as never)).toBe("READY");
1041
+ });
883
1042
  });
884
1043
 
885
1044
  // #1401 — #1397 made the status WORD honest; this carries the part that says