@opensoha/contracts 0.1.17 → 0.1.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.
Files changed (268) hide show
  1. package/COMPATIBILITY.md +45 -0
  2. package/README.md +37 -0
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/outpost_headers.go +17 -0
  5. package/auth/outpost_headers_test.go +16 -0
  6. package/auth/permission-catalog.json +383 -1
  7. package/compat/consumer-revisions.json +3 -0
  8. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  9. package/compat/openapi-loose-boundaries-0.1.0.json +78 -46
  10. package/delivery/canonical.go +136 -0
  11. package/delivery/document.go +203 -0
  12. package/delivery/document.schema.json +1527 -0
  13. package/delivery/document_test.go +191 -0
  14. package/delivery/parser.go +155 -0
  15. package/evaluation/evaluation-run.schema.json +64 -10
  16. package/fixtures/json-schema/gateway-manifest/invalid/bad-execution-mode.json +73 -0
  17. package/fixtures/json-schema/gateway-manifest/invalid/unversioned-check.json +85 -0
  18. package/fixtures/json-schema/gateway-manifest/valid/checks.json +86 -0
  19. package/fixtures/json-schema/gateway-manifest/valid/versioned-task.json +73 -0
  20. package/fixtures/json-schema/network-ingest-event/invalid/vpn-probe-negative-rtt.json +34 -0
  21. package/fixtures/json-schema/network-ingest-event/invalid/vpn-stats-secret.json +27 -0
  22. package/fixtures/json-schema/network-ingest-event/valid/vpn-probe-batch.json +45 -0
  23. package/fixtures/json-schema/network-ingest-event/valid/vpn-tunnel-stats.json +26 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-bad-token.json +15 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/vpn-managed-mode-override.json +16 -0
  26. package/fixtures/json-schema/network-runtime-protocol/valid/managed-vpn-macos-enrollment.json +27 -0
  27. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-managed-connect.json +15 -0
  28. package/fixtures/openapi/AgentChatInput/invalid/empty-question.json +4 -0
  29. package/fixtures/openapi/AgentChatInput/invalid/system-role.json +9 -0
  30. package/fixtures/openapi/AgentChatInput/valid/authorized-background.json +5 -0
  31. package/fixtures/openapi/AgentChatInput/valid/conversation.json +15 -0
  32. package/fixtures/openapi/AgentRunnerToolCallRequest/invalid/model-identity.json +1 -0
  33. package/fixtures/openapi/AgentRunnerToolCallRequest/valid/search.json +1 -0
  34. package/fixtures/openapi/ApplicationInput/invalid/negative-version.json +6 -0
  35. package/fixtures/openapi/ApplicationInput/invalid/zero-version.json +6 -0
  36. package/fixtures/openapi/ApplicationInput/valid/versioned.json +6 -0
  37. package/fixtures/openapi/ApprovalRequest/invalid/resolved-scopes.json +20 -0
  38. package/fixtures/openapi/ApprovalRequest/valid/resolved-scopes.json +21 -0
  39. package/fixtures/openapi/BuildTemplateInput/invalid/negative-revision.json +6 -0
  40. package/fixtures/openapi/BuildTemplateInput/valid/draft.json +11 -0
  41. package/fixtures/openapi/BuildTriggerRequest/invalid/no-ref.json +1 -0
  42. package/fixtures/openapi/BuildTriggerRequest/valid/no-environment.json +1 -0
  43. package/fixtures/openapi/BuildpacksConfiguration/invalid/mutable-builder.json +5 -0
  44. package/fixtures/openapi/BuildpacksConfiguration/invalid/privilege-override.json +6 -0
  45. package/fixtures/openapi/BuildpacksConfiguration/invalid/unsupported-platform.json +5 -0
  46. package/fixtures/openapi/BuildpacksConfiguration/valid/pinned.json +5 -0
  47. package/fixtures/openapi/CapabilityAssessment/invalid/contract.json +16 -0
  48. package/fixtures/openapi/CapabilityAssessment/valid/contract.json +16 -0
  49. package/fixtures/openapi/CapabilityPlan/invalid/contract.json +16 -0
  50. package/fixtures/openapi/CapabilityPlan/valid/contract.json +19 -0
  51. package/fixtures/openapi/CapabilityPlanValidation/invalid/contract.json +5 -0
  52. package/fixtures/openapi/CapabilityPlanValidation/valid/contract.json +11 -0
  53. package/fixtures/openapi/CapabilityTask/invalid/contract.json +33 -0
  54. package/fixtures/openapi/CapabilityTask/valid/contract.json +34 -0
  55. package/fixtures/openapi/CapabilityTaskInput/invalid/contract.json +22 -0
  56. package/fixtures/openapi/CapabilityTaskInput/valid/contract.json +22 -0
  57. package/fixtures/openapi/CapabilityValueSemantic/invalid/contract.json +4 -0
  58. package/fixtures/openapi/CapabilityValueSemantic/valid/contract.json +7 -0
  59. package/fixtures/openapi/DeliveryAccessResult/invalid/release-verification.json +8 -0
  60. package/fixtures/openapi/DeliveryAccessResult/valid/release-verification.json +8 -0
  61. package/fixtures/openapi/DeliveryBatchAssessment/invalid/release-verification.json +11 -0
  62. package/fixtures/openapi/DeliveryBatchAssessment/valid/release-verification.json +11 -0
  63. package/fixtures/openapi/DeliveryBatchAssessmentInput/invalid/release-verification.json +9 -0
  64. package/fixtures/openapi/DeliveryBatchAssessmentInput/valid/release-verification.json +8 -0
  65. package/fixtures/openapi/DeliveryBatchInput/invalid/ambiguous-source.json +19 -0
  66. package/fixtures/openapi/DeliveryBatchInput/invalid/missing-idempotency.json +16 -0
  67. package/fixtures/openapi/DeliveryBatchInput/valid/quick-deploy.json +17 -0
  68. package/fixtures/openapi/DeliveryBatchInput/valid/saved-workflow.json +5 -0
  69. package/fixtures/openapi/DeliveryBlueprintInput/invalid/unversioned-preset.json +31 -0
  70. package/fixtures/openapi/DeliveryBlueprintInput/valid/service-preset.json +32 -0
  71. package/fixtures/openapi/DeliveryDocument/invalid/unknown-field.json +24 -0
  72. package/fixtures/openapi/DeliveryDocument/invalid/unknown-kind.json +23 -0
  73. package/fixtures/openapi/DeliveryDocument/invalid/unknown-version.json +23 -0
  74. package/fixtures/openapi/DeliveryDocument/valid/build-template.json +26 -0
  75. package/fixtures/openapi/DeliveryDocument/valid/deployment-template.json +26 -0
  76. package/fixtures/openapi/DeliveryDocument/valid/legacy-dag.json +39 -0
  77. package/fixtures/openapi/DeliveryDocument/valid/workflow-template.json +23 -0
  78. package/fixtures/openapi/DeliveryDocument/valid/workflow.json +20 -0
  79. package/fixtures/openapi/DeliveryDraftInput/invalid/short-idempotency-key.json +30 -0
  80. package/fixtures/openapi/DeliveryDraftInput/valid/idempotent-create.json +30 -0
  81. package/fixtures/openapi/DeliveryDraftService/invalid/zero-version.json +7 -0
  82. package/fixtures/openapi/DeliveryDraftService/valid/versioned-template.json +9 -0
  83. package/fixtures/openapi/DeliveryExecutionHistoryPage/invalid-kind.json +9 -0
  84. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-build.json +16 -0
  85. package/fixtures/openapi/DeliveryExecutionHistoryPage/valid-empty.json +3 -0
  86. package/fixtures/openapi/DeliveryPlanApprovalInput/invalid/confirm.json +1 -0
  87. package/fixtures/openapi/DeliveryPlanApprovalInput/valid/approve.json +1 -0
  88. package/fixtures/openapi/DeliveryPlanInput/invalid/draft-manifest-revision.json +1 -0
  89. package/fixtures/openapi/DeliveryPlanInput/valid/manifest-revision.json +1 -0
  90. package/fixtures/openapi/DeliveryTargetInput/invalid/deploy-without-environment.json +7 -0
  91. package/fixtures/openapi/DeliveryTargetInput/valid/build-without-environment.json +6 -0
  92. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/duplicate-kind.json +13 -0
  93. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/embedded-credentials.json +16 -0
  94. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/empty-kinds.json +10 -0
  95. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/negative-generation.json +15 -0
  96. package/fixtures/openapi/DeliveryTemplateSourceInput/invalid/unknown-kind.json +12 -0
  97. package/fixtures/openapi/DeliveryTemplateSourceInput/valid/manual-source.json +15 -0
  98. package/fixtures/openapi/DeliveryWorkflowDefinition/invalid/unversioned-template.json +15 -0
  99. package/fixtures/openapi/DeliveryWorkflowDefinition/valid/cross-app.json +24 -0
  100. package/fixtures/openapi/DeliveryWorkflowInput/invalid/short-key.json +1 -0
  101. package/fixtures/openapi/DeliveryWorkflowInput/valid/idempotent-create.json +1 -0
  102. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-target-override.json +7 -0
  103. package/fixtures/openapi/DeploymentTemplatePreviewInput/invalid/invalid-zero-version.json +6 -0
  104. package/fixtures/openapi/DeploymentTemplatePreviewInput/valid/valid.json +6 -0
  105. package/fixtures/openapi/DockerDeliveryConfiguration/invalid/empty-mappings.json +5 -0
  106. package/fixtures/openapi/DockerDeliveryConfiguration/valid/compose.json +7 -0
  107. package/fixtures/openapi/DockerDeliverySnapshot/invalid/missing-required-field.json +19 -0
  108. package/fixtures/openapi/DockerDeliverySnapshot/valid/frozen-project.json +20 -0
  109. package/fixtures/openapi/DockerOperationCallbackRequest/invalid/cancellation-acknowledged-not-boolean.json +13 -0
  110. package/fixtures/openapi/DockerOperationCallbackRequest/valid/cancellation-acknowledged.json +13 -0
  111. package/fixtures/openapi/DockerProjectInput/invalid/empty-host.json +4 -0
  112. package/fixtures/openapi/DockerProjectInput/valid/inline-idempotent.json +7 -0
  113. package/fixtures/openapi/DockerQuickCreateHostInput/invalid/capacity-type.json +1 -0
  114. package/fixtures/openapi/DockerQuickCreateHostInput/valid/capacity.json +1 -0
  115. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/plain-credential.json +21 -0
  116. package/fixtures/openapi/HelmDeliveryConfiguration/invalid/release-name.json +20 -0
  117. package/fixtures/openapi/HelmDeliveryConfiguration/valid/private-oci.json +20 -0
  118. package/fixtures/openapi/HelmDeliverySnapshot/invalid/secret-plan.json +31 -0
  119. package/fixtures/openapi/HelmDeliverySnapshot/invalid/unknown-operation.json +28 -0
  120. package/fixtures/openapi/HelmDeliverySnapshot/valid/first-install.json +28 -0
  121. package/fixtures/openapi/IdentityApplication/invalid/outpost-status.json +1 -0
  122. package/fixtures/openapi/IdentityApplicationOnboardingInput/invalid/missing-access.json +12 -0
  123. package/fixtures/openapi/IdentityApplicationOnboardingInput/valid/link.json +13 -0
  124. package/fixtures/openapi/IdentityOutpost/invalid/missing-runtime-status.json +9 -0
  125. package/fixtures/openapi/IdentityOutpost/valid/healthy.json +17 -0
  126. package/fixtures/openapi/IdentityOutpost/valid/unregistered.json +12 -0
  127. package/fixtures/openapi/IdentityOutpostDeployment/invalid/missing-protocol.json +3 -0
  128. package/fixtures/openapi/IdentityOutpostDeployment/valid/remote.json +6 -0
  129. package/fixtures/openapi/IdentityOutpostInput/invalid/forged-observation.json +7 -0
  130. package/fixtures/openapi/IdentityOutpostInput/valid/forward-auth.json +6 -0
  131. package/fixtures/openapi/IdentityOutpostInput/valid/legacy.json +8 -0
  132. package/fixtures/openapi/IdentityProviderSetup/invalid/fake-verification.json +1 -0
  133. package/fixtures/openapi/IdentityProviderSetup/valid/incomplete.json +1 -0
  134. package/fixtures/openapi/IdentityProviderSetup/valid/remote.json +1 -0
  135. package/fixtures/openapi/IdentityProviderUserMetadata/invalid/attributes.json +8 -0
  136. package/fixtures/openapi/IdentityProviderUserMetadata/valid/attributes.json +13 -0
  137. package/fixtures/openapi/KubernetesCustomResource/invalid/negative-generation.json +1 -0
  138. package/fixtures/openapi/KubernetesCustomResource/valid/deleting-operator.json +1 -0
  139. package/fixtures/openapi/KubernetesResourceCreationReceipt/invalid/manifest-content.json +20 -0
  140. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/confirmed.json +20 -0
  141. package/fixtures/openapi/KubernetesResourceCreationReceipt/valid/unknown.json +19 -0
  142. package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/negative-resource-observation.json +57 -0
  143. package/fixtures/openapi/ManifestDeploymentEnvelope/valid/operator-deleting.json +57 -0
  144. package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/gitops-empty.json +7 -0
  145. package/fixtures/openapi/ManifestExecutionTaskPayload/valid/gitops-frozen-children.json +18 -0
  146. package/fixtures/openapi/ManifestKustomizeOptions/invalid/escaping-entry.json +1 -0
  147. package/fixtures/openapi/ManifestKustomizeOptions/invalid/mutable-image.json +1 -0
  148. package/fixtures/openapi/ManifestKustomizeOptions/valid/overlay-images.json +7 -0
  149. package/fixtures/openapi/ManifestRevisionInput/invalid/missing-version.json +1 -0
  150. package/fixtures/openapi/ManifestRevisionInput/valid/current-draft.json +1 -0
  151. package/fixtures/openapi/ManifestServiceTemplateInputs/invalid/mutable-image.json +8 -0
  152. package/fixtures/openapi/ManifestServiceTemplateInputs/valid/frozen-service-image.json +9 -0
  153. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/auto-with-gateway.json +6 -0
  154. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/missing-manual-gateway.json +5 -0
  155. package/fixtures/openapi/NetworkVPNConnectionIntentInput/invalid/mode-override.json +7 -0
  156. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/auto-intent.json +5 -0
  157. package/fixtures/openapi/NetworkVPNConnectionIntentInput/valid/manual-intent.json +6 -0
  158. package/fixtures/openapi/NetworkVPNProfileInput/invalid/unknown-private-key.json +25 -0
  159. package/fixtures/openapi/NetworkVPNProfileInput/valid/assigned-profile.json +24 -0
  160. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/invalid/unbounded-attempts.json +21 -0
  161. package/fixtures/openapi/NetworkVPNSelectionPolicyInput/valid/provider-policy.json +21 -0
  162. package/fixtures/openapi/ObservabilityMetricAssessmentInput/invalid/arbitrary-expression.json +10 -0
  163. package/fixtures/openapi/ObservabilityMetricAssessmentInput/valid/metric-window.json +10 -0
  164. package/fixtures/openapi/PluginManifest/valid/hermes-api.json +53 -0
  165. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/missing-identity.json +5 -0
  166. package/fixtures/openapi/ProgressiveRolloutControlInput/invalid/skip-analysis.json +5 -0
  167. package/fixtures/openapi/ProgressiveRolloutControlInput/valid/pause.json +5 -0
  168. package/fixtures/openapi/ReleaseTargetInput/invalid/missing-required-field.json +15 -0
  169. package/fixtures/openapi/ReleaseTargetInput/valid/docker-compose.json +16 -0
  170. package/fixtures/openapi/RepositoryAnalysis/invalid/moving-ref.json +11 -0
  171. package/fixtures/openapi/RepositoryAnalysis/valid/go.json +29 -0
  172. package/fixtures/openapi/RepositoryAnalysisInput/invalid/arbitrary-source-url.json +6 -0
  173. package/fixtures/openapi/RepositoryAnalysisInput/invalid/missing-ref.json +4 -0
  174. package/fixtures/openapi/RepositoryAnalysisInput/valid/nested-project.json +6 -0
  175. package/fixtures/openapi/ServiceDeploymentTemplateBinding/invalid/invalid-unpinned.json +4 -0
  176. package/fixtures/openapi/ServiceDeploymentTemplateBinding/valid/valid.json +7 -0
  177. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-executable-schema.json +25 -0
  178. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-missing-schema.json +21 -0
  179. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/invalid-mixed-renderers.json +40 -0
  180. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-nested-parameters.json +41 -0
  181. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/valid-yaml.json +34 -0
  182. package/fixtures/openapi/TemplatePublishInput/invalid/missing-revision.json +1 -0
  183. package/fixtures/openapi/TemplatePublishInput/invalid/unknown-field.json +4 -0
  184. package/fixtures/openapi/TemplatePublishInput/invalid/zero-revision.json +3 -0
  185. package/fixtures/openapi/TemplatePublishInput/valid/revision.json +3 -0
  186. package/fixtures/openapi/ToolCapability/invalid/bad-idempotency.json +35 -0
  187. package/fixtures/openapi/ToolCapability/invalid/unversioned-check.json +47 -0
  188. package/fixtures/openapi/ToolCapability/valid/checks.json +48 -0
  189. package/fixtures/openapi/ToolCapability/valid/versioned-task.json +35 -0
  190. package/fixtures/openapi/ToolInvocationRequest/invalid/empty-version.json +8 -0
  191. package/fixtures/openapi/ToolInvocationRequest/valid/versioned.json +8 -0
  192. package/fixtures/openapi/ToolInvocationResult/invalid/missing-status-call.json +13 -0
  193. package/fixtures/openapi/ToolInvocationResult/valid/task.json +20 -0
  194. package/fixtures/openapi/VirtualMachineCreateInput/invalid/capacity-type.json +1 -0
  195. package/fixtures/openapi/VirtualMachineCreateInput/valid/capacity.json +1 -0
  196. package/fixtures/openapi/VirtualizationCapacityInput/invalid/missing-disk.json +1 -0
  197. package/fixtures/openapi/VirtualizationCapacityInput/valid/running-vm.json +1 -0
  198. package/fixtures/openapi/VirtualizationCapacityResult/invalid/invented-status.json +1 -0
  199. package/fixtures/openapi/VirtualizationCapacityResult/valid/available.json +1 -0
  200. package/fixtures/openapi/VirtualizationWorkerCreateInput/invalid/secret-input.json +5 -0
  201. package/fixtures/openapi/VirtualizationWorkerCreateInput/valid/one-worker.json +4 -0
  202. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/external-owner.json +27 -0
  203. package/fixtures/openapi/VirtualizationWorkerPoolInput/invalid/zero-budget.json +27 -0
  204. package/fixtures/openapi/VirtualizationWorkerPoolInput/valid/registered.json +27 -0
  205. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/invalid/nested-data.json +5 -0
  206. package/fixtures/openapi/VirtualizationWorkerPoolListEnvelope/valid/empty.json +3 -0
  207. package/fixtures/openapi/WorkbenchContextSelection/invalid/missing-file-content.json +8 -0
  208. package/fixtures/openapi/WorkbenchContextSelection/invalid/unknown-reference-kind.json +8 -0
  209. package/fixtures/openapi/WorkbenchContextSelection/valid/text-and-resource.json +17 -0
  210. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/trigger-without-plan.json +10 -0
  211. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/unknown-trigger.json +28 -0
  212. package/fixtures/openapi/WorkbenchInspectionTaskInput/invalid/zero-interval.json +28 -0
  213. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/alert-capability.json +30 -0
  214. package/fixtures/openapi/WorkbenchInspectionTaskInput/valid/scheduled-capability.json +28 -0
  215. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/missing-rule.json +3 -0
  216. package/fixtures/openapi/WorkbenchInspectionTrigger/invalid/schedule-alert-selector.json +4 -0
  217. package/fixtures/openapi/WorkbenchInspectionTrigger/valid/schedule.json +3 -0
  218. package/fixtures/openapi/WorkbenchModelPreferences/invalid/unknown-effort.json +1 -0
  219. package/fixtures/openapi/WorkbenchModelPreferences/valid/selected.json +1 -0
  220. package/fixtures/openapi/WorkflowCatalogPage/invalid/unsupported-kind.json +6 -0
  221. package/fixtures/openapi/WorkflowCatalogPage/valid/build-without-environment.json +6 -0
  222. package/fixtures/openapi/WorkflowTemplateInput/invalid/zero-revision.json +6 -0
  223. package/fixtures/openapi/WorkflowTemplateInput/valid/draft.json +10 -0
  224. package/gen/go/sohaapi/client.go +1 -1
  225. package/gen/go/sohaapi/types.go +8070 -859
  226. package/gen/go/sohaapi/types_test.go +100 -1
  227. package/gen/go/sohaapi/vpn_models_test.go +21 -0
  228. package/gen/ts/sohaapi/index.ts +20343 -13162
  229. package/go.mod +107 -5
  230. package/go.sum +379 -16
  231. package/helmrelease/delivery.go +283 -0
  232. package/helmrelease/delivery_test.go +102 -0
  233. package/helmrelease/frozen_chart.go +91 -0
  234. package/helmrelease/result.go +27 -0
  235. package/helmrelease/runtime/delivery.go +508 -0
  236. package/helmrelease/runtime/delivery_test.go +236 -0
  237. package/helmrelease/runtime/resources_test.go +60 -0
  238. package/helmrelease/values.go +99 -0
  239. package/knowledge/context-envelope.schema.json +1 -0
  240. package/mcp/gateway-manifest.schema.json +387 -72
  241. package/network/README.md +40 -0
  242. package/network/network-ingest-event.schema.json +228 -0
  243. package/network/network-runtime-protocol.schema.json +457 -1
  244. package/openapi/soha-api.json +54958 -38899
  245. package/openapi/soha-api.yaml +12143 -4984
  246. package/openapi/spec_generated.go +3 -3
  247. package/package.json +3 -1
  248. package/resource/manifest_health.go +155 -0
  249. package/resource/manifest_health_test.go +37 -0
  250. package/resource/models.go +14 -0
  251. package/resource/runtime/argocd.go +166 -0
  252. package/resource/runtime/argocd_integration_test.go +142 -0
  253. package/resource/runtime/argocd_observation.go +143 -0
  254. package/resource/runtime/argocd_operation.go +253 -0
  255. package/resource/runtime/argocd_resources.go +115 -0
  256. package/resource/runtime/argocd_resources_test.go +107 -0
  257. package/resource/runtime/argocd_test.go +386 -0
  258. package/resource/runtime/health.go +115 -0
  259. package/resource/runtime/health_test.go +78 -0
  260. package/resource/runtime/operator_integration_test.go +174 -0
  261. package/resource/runtime/ownership.go +178 -0
  262. package/resource/runtime/ownership_test.go +178 -0
  263. package/resource/runtime/rollout_integration_test.go +287 -0
  264. package/resource/runtime/rollout_observation.go +252 -0
  265. package/resource/runtime/rollout_operation.go +398 -0
  266. package/resource/runtime/rollout_resources.go +385 -0
  267. package/resource/runtime/rollout_task.go +165 -0
  268. package/resource/runtime/rollout_test.go +450 -0
@@ -0,0 +1,450 @@
1
+ package runtime
2
+
3
+ import (
4
+ "context"
5
+ "crypto/sha256"
6
+ "encoding/hex"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "strings"
11
+ "testing"
12
+ "time"
13
+
14
+ "github.com/opensoha/soha-contracts/gen/go/sohaapi"
15
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
16
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18
+ "k8s.io/apimachinery/pkg/runtime"
19
+ "k8s.io/apimachinery/pkg/runtime/schema"
20
+ "k8s.io/apimachinery/pkg/types"
21
+ "k8s.io/client-go/dynamic/fake"
22
+ ktesting "k8s.io/client-go/testing"
23
+ )
24
+
25
+ func rolloutFixture(t *testing.T, strategy string) []*unstructured.Unstructured {
26
+ t.Helper()
27
+ var documents []*unstructured.Unstructured
28
+ err := json.Unmarshal([]byte(`[
29
+ {"apiVersion":"argoproj.io/v1alpha1","kind":"Rollout","metadata":{"name":"web","namespace":"demo"},"spec":{"replicas":2,"selector":{"matchLabels":{"app":"web"}},"template":{"metadata":{"labels":{"app":"web"}},"spec":{"containers":[{"name":"app","image":"registry.example/app@sha256:`+strings.Repeat("a", 64)+`"}]}},"strategy":{"canary":{"stableService":"web-stable","canaryService":"web-preview","trafficRouting":{"traefik":{"weightedTraefikServiceName":"web-traffic"}},"steps":[{"setWeight":20},{"pause":{}},{"analysis":{"templates":[{"templateName":"web-metrics"}]}},{"setWeight":100}]}}}},
30
+ {"apiVersion":"v1","kind":"Service","metadata":{"name":"web-stable","namespace":"demo"},"spec":{"selector":{"app":"web"},"ports":[{"port":8080,"targetPort":8080}]}},
31
+ {"apiVersion":"v1","kind":"Service","metadata":{"name":"web-preview","namespace":"demo"},"spec":{"selector":{"app":"web"},"ports":[{"port":8080,"targetPort":8080}]}},
32
+ {"apiVersion":"argoproj.io/v1alpha1","kind":"AnalysisTemplate","metadata":{"name":"web-metrics","namespace":"demo"},"spec":{"metrics":[{"name":"success","interval":"2s","count":3,"successCondition":"result >= 0.99","failureLimit":0,"provider":{"web":{"url":"http://web-preview.demo.svc:8080/metrics","jsonPath":"{$.successRate}"}}}]}},
33
+ {"apiVersion":"traefik.io/v1alpha1","kind":"TraefikService","metadata":{"name":"web-traffic","namespace":"demo"},"spec":{"weighted":{"services":[{"name":"web-stable","port":8080,"weight":100},{"name":"web-preview","port":8080,"weight":0}]}}},
34
+ {"apiVersion":"traefik.io/v1alpha1","kind":"IngressRoute","metadata":{"name":"web-route","namespace":"demo"},"spec":{"entryPoints":["web"],"routes":[{"kind":"Rule","match":"PathPrefix(\"/\")","services":[{"name":"web-traffic","kind":"TraefikService"}]}]}}
35
+ ]`), &documents)
36
+ if err != nil {
37
+ t.Fatal(err)
38
+ }
39
+ if strategy == "blueGreen" {
40
+ documents = documents[:4]
41
+ _ = unstructured.SetNestedMap(documents[0].Object, map[string]any{"blueGreen": map[string]any{"activeService": "web-stable", "previewService": "web-preview", "autoPromotionEnabled": false, "prePromotionAnalysis": map[string]any{"templates": []any{map[string]any{"templateName": "web-metrics"}}}}}, "spec", "strategy")
42
+ }
43
+ return documents
44
+ }
45
+
46
+ func rolloutLiveFixture(t *testing.T) ([]*unstructured.Unstructured, *fake.FakeDynamicClient) {
47
+ documents := rolloutFixture(t, "canary")
48
+ objects := make([]runtime.Object, 0, len(documents))
49
+ for index, document := range documents {
50
+ live := document.DeepCopy()
51
+ live.SetUID(types.UID(fmt.Sprintf("uid-%d", index)))
52
+ live.SetResourceVersion("42")
53
+ live.SetAnnotations(map[string]string{RolloutOwnerAnnotation: "opensoha-manifest/test:web"})
54
+ if IsArgoRollout(live) {
55
+ live.SetGeneration(3)
56
+ annotations := live.GetAnnotations()
57
+ annotations[RolloutOperationAnnotation] = "operation-1"
58
+ live.SetAnnotations(annotations)
59
+ live.Object["status"] = map[string]any{"observedGeneration": "3", "phase": "Paused", "currentPodHash": "new", "stableRS": "old", "currentStepIndex": int64(1), "pauseConditions": []any{map[string]any{"reason": "CanaryPauseStep"}}}
60
+ }
61
+ if live.GetKind() == "Service" {
62
+ hash := "old"
63
+ if live.GetName() == "web-preview" {
64
+ hash = "new"
65
+ }
66
+ _ = unstructured.SetNestedField(live.Object, hash, "spec", "selector", rolloutPodHash)
67
+ }
68
+ if live.GetKind() == "TraefikService" {
69
+ _ = unstructured.SetNestedSlice(live.Object, []any{map[string]any{"name": "web-stable", "port": int64(8080), "weight": int64(80)}, map[string]any{"name": "web-preview", "port": int64(8080), "weight": int64(20)}}, "spec", "weighted", "services")
70
+ }
71
+ objects = append(objects, live)
72
+ }
73
+ return documents, fake.NewSimpleDynamicClient(runtime.NewScheme(), objects...)
74
+ }
75
+
76
+ func TestRolloutRestoreWaitsForCurrentGenerationBeforeAcceptingFailure(t *testing.T) {
77
+ for _, phase := range []string{"Degraded", "Aborted"} {
78
+ t.Run(phase, func(t *testing.T) {
79
+ documents, client := rolloutLiveFixture(t)
80
+ root, err := client.Resource(rollouts).Namespace("demo").Get(t.Context(), "web", metav1.GetOptions{})
81
+ operatorRequire(t, err)
82
+ root.SetGeneration(4)
83
+ if phase == "Aborted" {
84
+ _ = unstructured.SetNestedField(root.Object, true, "status", "abort")
85
+ } else {
86
+ _ = unstructured.SetNestedField(root.Object, phase, "status", "phase")
87
+ }
88
+ _, err = client.Resource(rollouts).Namespace("demo").Update(t.Context(), root, metav1.UpdateOptions{})
89
+ operatorRequire(t, err)
90
+ observed, err := ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
91
+ if err != nil || observed.Health != "progressing" {
92
+ t.Fatalf("previous generation failure must not abort a new restore: %+v, %v", observed, err)
93
+ }
94
+ _ = unstructured.SetNestedField(root.Object, "4", "status", "observedGeneration")
95
+ _, err = client.Resource(rollouts).Namespace("demo").Update(t.Context(), root, metav1.UpdateOptions{})
96
+ operatorRequire(t, err)
97
+ observed, err = ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
98
+ if err != nil || observed.Health != "degraded" {
99
+ t.Fatalf("current generation failure must remain terminal: %+v, %v", observed, err)
100
+ }
101
+ })
102
+ }
103
+ }
104
+
105
+ func TestRolloutObservationAcceptsOmittedEmptyContainerLists(t *testing.T) {
106
+ documents, client := rolloutLiveFixture(t)
107
+ containers, _, _ := unstructured.NestedSlice(documents[0].Object, "spec", "template", "spec", "containers")
108
+ container, ok := containers[0].(map[string]any)
109
+ if !ok {
110
+ t.Fatal("unexpected container")
111
+ }
112
+ container["env"] = []any{}
113
+ _ = unstructured.SetNestedSlice(documents[0].Object, containers, "spec", "template", "spec", "containers")
114
+ observed, err := ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
115
+ if err != nil || observed.Drifted || observed.Health != "progressing" {
116
+ t.Fatalf("omitted empty env must not abort a healthy progression: %+v, %v", observed, err)
117
+ }
118
+ container, ok = containers[0].(map[string]any)
119
+ if !ok {
120
+ t.Fatal("unexpected container")
121
+ }
122
+ container["env"] = []any{map[string]any{"name": "REQUIRED", "value": "yes"}}
123
+ _ = unstructured.SetNestedSlice(documents[0].Object, containers, "spec", "template", "spec", "containers")
124
+ observed, err = ObserveRollout(t.Context(), client, documents, "opensoha-manifest/test", "operation-1")
125
+ if err != nil || !observed.Drifted || observed.Health != "degraded" {
126
+ t.Fatalf("missing required env must still fail: %+v, %v", observed, err)
127
+ }
128
+ }
129
+
130
+ func TestRolloutFrozenResources(t *testing.T) {
131
+ for _, strategy := range []string{"canary", "blueGreen"} {
132
+ if _, err := ValidateRolloutResources(rolloutFixture(t, strategy)); err != nil {
133
+ t.Fatal(strategy, err)
134
+ }
135
+ }
136
+ for _, scenario := range []string{"missing service", "namespace", "mutable image", "job analysis", "external metric", "unbounded metric", "invalid port", "skip analysis", "second root", "controller owner", "backend port", "other traffic", "unreferenced", "preset pause"} {
137
+ t.Run(scenario, func(t *testing.T) {
138
+ documents := rolloutFixture(t, "canary")
139
+ switch scenario {
140
+ case "missing service":
141
+ documents = append(documents[:1], documents[2:]...)
142
+ case "namespace":
143
+ documents[1].SetNamespace("outside")
144
+ case "mutable image":
145
+ _ = unstructured.SetNestedSlice(documents[0].Object, []any{map[string]any{"name": "app", "image": "app:latest"}}, "spec", "template", "spec", "containers")
146
+ case "job analysis", "external metric", "unbounded metric":
147
+ metrics, _, _ := unstructured.NestedSlice(documents[3].Object, "spec", "metrics")
148
+ metric, ok := metrics[0].(map[string]any)
149
+ if !ok {
150
+ t.Fatal("unexpected metric")
151
+ }
152
+ switch scenario {
153
+ case "job analysis":
154
+ metric["provider"] = map[string]any{"job": map[string]any{}}
155
+ case "external metric":
156
+ metric["provider"] = map[string]any{"web": map[string]any{"url": "http://other.demo.svc/metrics"}}
157
+ case "unbounded metric":
158
+ metric["interval"] = "24h"
159
+ }
160
+ _ = unstructured.SetNestedSlice(documents[3].Object, metrics, "spec", "metrics")
161
+ case "skip analysis":
162
+ _ = unstructured.SetNestedSlice(documents[0].Object, []any{map[string]any{"setWeight": int64(100)}}, "spec", "strategy", "canary", "steps")
163
+ case "invalid port":
164
+ documents = rolloutFixture(t, "blueGreen")
165
+ _ = unstructured.SetNestedSlice(documents[1].Object, []any{"invalid"}, "spec", "ports")
166
+ case "second root":
167
+ other := documents[0].DeepCopy()
168
+ other.SetName("other")
169
+ documents = append(documents, other)
170
+ case "controller owner":
171
+ documents[1].SetAnnotations(map[string]string{RolloutOwnerAnnotation: "another"})
172
+ case "backend port":
173
+ _ = unstructured.SetNestedSlice(documents[1].Object, []any{map[string]any{"port": int64(9090)}}, "spec", "ports")
174
+ case "other traffic":
175
+ _ = unstructured.SetNestedMap(documents[0].Object, map[string]any{"nginx": map[string]any{}}, "spec", "strategy", "canary", "trafficRouting")
176
+ case "unreferenced":
177
+ other := documents[1].DeepCopy()
178
+ other.SetName("other")
179
+ documents = append(documents, other)
180
+ case "preset pause":
181
+ _ = unstructured.SetNestedField(documents[0].Object, true, "spec", "paused")
182
+ }
183
+ if _, err := ValidateRolloutResources(documents); err == nil {
184
+ t.Fatal("unsafe bundle accepted")
185
+ }
186
+ })
187
+ }
188
+ }
189
+
190
+ func TestRolloutControlsUseIdentityAndCannotSkipGates(t *testing.T) {
191
+ for _, scenario := range []string{"promote", "pause", "abort", "stale UID", "stale RV", "inconclusive", "timed pause", "other owner", "other operation"} {
192
+ t.Run(scenario, func(t *testing.T) {
193
+ documents, client := rolloutLiveFixture(t)
194
+ root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
195
+ input := sohaapi.ProgressiveRolloutControlInput{Action: "promote", UID: "uid-0", ResourceVersion: "42"}
196
+ switch scenario {
197
+ case "pause", "abort":
198
+ input.Action = sohaapi.ProgressiveRolloutControlInputAction(scenario)
199
+ case "stale UID":
200
+ input.UID = "replaced"
201
+ case "stale RV":
202
+ input.ResourceVersion = "41"
203
+ case "inconclusive":
204
+ _ = unstructured.SetNestedSlice(root.Object, []any{map[string]any{"reason": "InconclusiveAnalysis"}}, "status", "pauseConditions")
205
+ case "timed pause":
206
+ steps, _, _ := unstructured.NestedSlice(root.Object, "spec", "strategy", "canary", "steps")
207
+ steps[1] = map[string]any{"pause": map[string]any{"duration": "1m"}}
208
+ _ = unstructured.SetNestedSlice(root.Object, steps, "spec", "strategy", "canary", "steps")
209
+ _ = unstructured.SetNestedSlice(documents[0].Object, steps, "spec", "strategy", "canary", "steps")
210
+ case "other owner":
211
+ annotations := root.GetAnnotations()
212
+ annotations[RolloutOwnerAnnotation] = "opensoha-manifest/test:other-root"
213
+ root.SetAnnotations(annotations)
214
+ case "other operation":
215
+ annotations := root.GetAnnotations()
216
+ annotations[RolloutOperationAnnotation] = "new-operation"
217
+ root.SetAnnotations(annotations)
218
+ }
219
+ _ = client.Tracker().Update(rollouts, root, "demo")
220
+ writes := 0
221
+ client.PrependReactor("patch", "rollouts", func(action ktesting.Action) (bool, runtime.Object, error) {
222
+ writes++
223
+ patch, ok := action.(ktesting.PatchAction)
224
+ if !ok {
225
+ t.Fatal("unexpected patch action")
226
+ }
227
+ var body map[string]any
228
+ _ = json.Unmarshal(patch.GetPatch(), &body)
229
+ metadata, ok := body["metadata"].(map[string]any)
230
+ if !ok {
231
+ t.Fatal("unexpected patch metadata")
232
+ }
233
+ if metadata["uid"] != "uid-0" || metadata["resourceVersion"] != "42" {
234
+ t.Fatal("control lost its CAS")
235
+ }
236
+ if status, ok := body["status"].(map[string]any); ok {
237
+ if status["currentStepIndex"] != nil || status["promoteFull"] != nil {
238
+ t.Fatal("control skipped native analysis")
239
+ }
240
+ }
241
+ return true, root, nil
242
+ })
243
+ _, err := ControlRollout(context.Background(), client, documents, "opensoha-manifest/test", "operation-1", input)
244
+ accepted := scenario == "promote" || scenario == "pause" || scenario == "abort"
245
+ if accepted && (err != nil || writes != 1) || !accepted && (err == nil || writes != 0) {
246
+ t.Fatalf("writes=%d err=%v", writes, err)
247
+ }
248
+ })
249
+ }
250
+ }
251
+
252
+ func TestRolloutStopRequiresActualStableTraffic(t *testing.T) {
253
+ documents, client := rolloutLiveFixture(t)
254
+ root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
255
+ _ = unstructured.SetNestedField(root.Object, true, "status", "abort")
256
+ _ = unstructured.SetNestedField(root.Object, "Degraded", "status", "phase")
257
+ _ = client.Tracker().Update(rollouts, root, "demo")
258
+ ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
259
+ defer cancel()
260
+ if err := StopRollout(ctx, client, documents, "opensoha-manifest/test", "operation-1", "uid-0"); !errors.Is(err, ErrRolloutStopUnconfirmed) {
261
+ t.Fatalf("unconfirmed traffic stop: %v", err)
262
+ }
263
+ gvr := rolloutResourceTypes["traefik.io/TraefikService"]
264
+ weighted, _ := client.Resource(gvr).Namespace("demo").Get(context.Background(), "web-traffic", metav1.GetOptions{})
265
+ _ = unstructured.SetNestedSlice(weighted.Object, []any{map[string]any{"name": "web-stable", "port": int64(8080), "weight": int64(100)}, map[string]any{"name": "web-preview", "port": int64(8080), "weight": int64(0)}}, "spec", "weighted", "services")
266
+ _ = client.Tracker().Update(gvr, weighted, "demo")
267
+ if err := StopRollout(context.Background(), client, documents, "opensoha-manifest/test", "operation-1", "uid-0"); err != nil {
268
+ t.Fatal(err)
269
+ }
270
+ if err := ValidateDirectManifestOwner(root); !errors.Is(err, ErrResourceOwnership) {
271
+ t.Fatal("native mutation accepted a Rollout")
272
+ }
273
+ if _, err := ApplyManifest(context.Background(), client.Resource(rollouts).Namespace("demo"), documents[0], metav1.PatchOptions{}); !errors.Is(err, ErrResourceOwnership) {
274
+ t.Fatal("generic apply accepted a Rollout")
275
+ }
276
+ }
277
+
278
+ func TestRolloutPreflightPreservesControllerFields(t *testing.T) {
279
+ documents, client := rolloutLiveFixture(t)
280
+ root, _ := client.Resource(rollouts).Namespace("demo").Get(context.Background(), "web", metav1.GetOptions{})
281
+ _ = unstructured.SetNestedField(root.Object, "Healthy", "status", "phase")
282
+ _ = client.Tracker().Update(rollouts, root, "demo")
283
+ writes := 0
284
+ client.PrependReactor("update", "*", func(action ktesting.Action) (bool, runtime.Object, error) {
285
+ update, ok := action.(ktesting.UpdateAction)
286
+ if !ok {
287
+ t.Fatal("unexpected update action")
288
+ }
289
+ write, ok := update.GetObject().(*unstructured.Unstructured)
290
+ if !ok {
291
+ t.Fatal("unexpected update object")
292
+ }
293
+ updateOptions, ok := action.(interface{ GetUpdateOptions() metav1.UpdateOptions })
294
+ if !ok {
295
+ t.Fatal("unexpected update options")
296
+ }
297
+ if write.GetUID() == "" || write.GetResourceVersion() != "42" || len(updateOptions.GetUpdateOptions().DryRun) != 1 {
298
+ t.Fatal("preflight lost identity or dry-run")
299
+ }
300
+ if write.GetKind() == "Service" && argoString(write, "spec", "selector", rolloutPodHash) == "" {
301
+ t.Fatal("preflight removed the native selector hash")
302
+ }
303
+ if write.GetKind() == "TraefikService" {
304
+ t.Fatal("preflight rewrote native traffic weights")
305
+ }
306
+ writes++
307
+ return true, write, nil
308
+ })
309
+ _, err := ApplyRollout(context.Background(), client, documents, metav1.PatchOptions{FieldManager: "opensoha-manifest/test", DryRun: []string{metav1.DryRunAll}}, "operation-2")
310
+ if err != nil || writes != 5 {
311
+ t.Fatalf("preflight writes=%d err=%v", writes, err)
312
+ }
313
+ }
314
+
315
+ func TestRolloutDependencyUpdateRetriesAndSkipsUnchanged(t *testing.T) {
316
+ documents, client := rolloutLiveFixture(t)
317
+ resources := client.Resource(rolloutResourceTypes["/Service"]).Namespace("demo")
318
+ live, err := resources.Get(t.Context(), "web-preview", metav1.GetOptions{})
319
+ if err != nil {
320
+ t.Fatal(err)
321
+ }
322
+ wanted := documents[2].DeepCopy()
323
+ _ = unstructured.SetNestedSlice(live.Object, []any{map[string]any{"port": int64(8080), "targetPort": int64(8080), "protocol": "TCP"}}, "spec", "ports")
324
+ _ = client.Tracker().Update(rolloutResourceTypes["/Service"], live, "demo")
325
+ wanted.SetAnnotations(live.GetAnnotations())
326
+ options := metav1.PatchOptions{FieldManager: "opensoha-manifest/test"}
327
+ if _, err := updateRolloutResource(t.Context(), resources, wanted, live, options, "web"); err != nil {
328
+ t.Fatal(err)
329
+ }
330
+ for _, action := range client.Actions() {
331
+ if action.GetVerb() == "update" {
332
+ t.Fatal("unchanged Service was rewritten")
333
+ }
334
+ }
335
+ _ = unstructured.SetNestedSlice(wanted.Object, []any{map[string]any{"port": int64(8080), "targetPort": int64(9090)}}, "spec", "ports")
336
+ writes := 0
337
+ client.PrependReactor("update", "services", func(action ktesting.Action) (bool, runtime.Object, error) {
338
+ writes++
339
+ if writes < 4 {
340
+ return true, nil, apierrors.NewConflict(rolloutResourceTypes["/Service"].GroupResource(), "web-preview", errors.New("native controller changed selector"))
341
+ }
342
+ update, ok := action.(ktesting.UpdateAction)
343
+ if !ok {
344
+ t.Fatal("unexpected update action")
345
+ }
346
+ write, ok := update.GetObject().(*unstructured.Unstructured)
347
+ if !ok {
348
+ t.Fatal("unexpected update object")
349
+ }
350
+ if argoString(write, "spec", "selector", rolloutPodHash) != "new" {
351
+ t.Fatal("native selector lost during retry")
352
+ }
353
+ return true, write, nil
354
+ })
355
+ if _, err := updateRolloutResource(t.Context(), resources, wanted, live, options, "web"); err != nil || writes != 4 {
356
+ t.Fatalf("writes=%d err=%v", writes, err)
357
+ }
358
+ }
359
+
360
+ func TestRolloutTaskValidatesFrozenPayloadAndReturnsNativeEvidence(t *testing.T) {
361
+ for _, scenario := range []string{"observe", "digest", "namespace", "owner", "mixed GitOps", "adopt"} {
362
+ t.Run(scenario, func(t *testing.T) {
363
+ documents, client := rolloutLiveFixture(t)
364
+ payload := sohaapi.ManifestExecutionTaskPayload{Action: sohaapi.ManifestTaskActionObserve, PackageID: "package", BindingID: "test", DeploymentID: "deployment", Generation: 7, Namespace: "demo", FieldManager: "opensoha-manifest/test", IdempotencyKey: "operation-1", RenderedDigest: "frozen"}
365
+ for _, item := range documents {
366
+ content, _ := item.MarshalJSON()
367
+ sum := sha256.Sum256(content)
368
+ payload.Documents = append(payload.Documents, sohaapi.ManifestRenderedDocument{APIVersion: item.GetAPIVersion(), Kind: item.GetKind(), Namespace: item.GetNamespace(), Name: item.GetName(), Content: string(content), ContentDigest: hex.EncodeToString(sum[:])})
369
+ }
370
+ switch scenario {
371
+ case "digest":
372
+ payload.Documents[1].Content += " "
373
+ case "namespace":
374
+ payload.Namespace = "outside"
375
+ case "owner":
376
+ payload.FieldManager = "opensoha-manifest/another"
377
+ case "mixed GitOps":
378
+ payload.GitOpsDocuments = payload.Documents[:1]
379
+ case "adopt":
380
+ payload.Action = sohaapi.ManifestTaskActionAdopt
381
+ }
382
+ result, err := ExecuteRolloutTask(t.Context(), client, payload)
383
+ if scenario != "observe" {
384
+ if err == nil || len(client.Actions()) != 0 {
385
+ t.Fatalf("invalid task reached Kubernetes: %v", err)
386
+ }
387
+ return
388
+ }
389
+ if err != nil {
390
+ t.Fatal(err)
391
+ }
392
+ if result.Rollout == nil || *result.Rollout.CanaryWeight != 20 || len(result.Inventory) != 6 || result.Drift == nil || result.Drift.Drifted {
393
+ t.Fatalf("missing native evidence: %+v", result)
394
+ }
395
+ for _, item := range result.Inventory {
396
+ if item.UID == "" || item.Generation != 7 || item.DesiredObjectDigest != item.ObservedObjectDigest {
397
+ t.Fatalf("bad inventory: %+v", item)
398
+ }
399
+ if item.Kind != "Service" && item.Health != "progressing" {
400
+ t.Fatalf("native policy readiness must follow the paused rollout: %+v", item)
401
+ }
402
+ }
403
+ })
404
+ }
405
+ }
406
+
407
+ func TestRolloutAnalysisEvidenceSurvivesStepCompletion(t *testing.T) {
408
+ root := rolloutFixture(t, "canary")[0]
409
+ root.SetUID("current-owner")
410
+ root.SetAnnotations(map[string]string{"rollout.argoproj.io/revision": "2"})
411
+ base := &unstructured.Unstructured{Object: map[string]any{
412
+ "apiVersion": "argoproj.io/v1alpha1", "kind": "AnalysisRun",
413
+ "metadata": map[string]any{"name": "current", "namespace": "demo", "uid": "run-uid", "labels": map[string]any{rolloutPodHash: "current-hash"}, "annotations": map[string]any{"rollout.argoproj.io/revision": "2"}},
414
+ "spec": map[string]any{"metrics": []any{map[string]any{"name": "success", "count": int64(2), "interval": "3s", "successCondition": "result >= 0.99"}}},
415
+ "status": map[string]any{"phase": "Successful", "metricResults": []any{map[string]any{"name": "success", "phase": "Successful", "count": int64(2), "successful": int64(2), "measurements": []any{
416
+ map[string]any{"startedAt": "2026-09-14T00:00:00Z", "finishedAt": "2026-09-14T00:00:01Z", "value": "0.99"},
417
+ map[string]any{"startedAt": "2026-09-14T00:00:03Z", "finishedAt": "2026-09-14T00:00:04Z", "value": "1"},
418
+ }}}},
419
+ }}
420
+ base.SetOwnerReferences([]metav1.OwnerReference{*metav1.NewControllerRef(root, root.GroupVersionKind())})
421
+ objects := []runtime.Object{base}
422
+ for _, mode := range []string{"old-owner", "old-revision", "old-hash"} {
423
+ run := base.DeepCopy()
424
+ run.SetName(mode)
425
+ run.SetUID(types.UID(mode))
426
+ switch mode {
427
+ case "old-owner":
428
+ refs := run.GetOwnerReferences()
429
+ refs[0].UID = "old-owner"
430
+ run.SetOwnerReferences(refs)
431
+ case "old-revision":
432
+ run.SetAnnotations(map[string]string{"rollout.argoproj.io/revision": "1"})
433
+ case "old-hash":
434
+ run.SetLabels(map[string]string{rolloutPodHash: "old-hash"})
435
+ }
436
+ objects = append(objects, run)
437
+ }
438
+ client := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), map[schema.GroupVersionResource]string{analysisRuns: "AnalysisRunList"}, objects...)
439
+ state := sohaapi.ProgressiveRolloutStatus{CurrentRevision: "current-hash"}
440
+ if err := observeRolloutAnalyses(t.Context(), client, root, &state); err != nil {
441
+ t.Fatal(err)
442
+ }
443
+ if len(state.Metrics) != 1 {
444
+ t.Fatalf("included unrelated analysis: %+v", state.Metrics)
445
+ }
446
+ metric := state.Metrics[0]
447
+ if metric.AnalysisRun != "current" || metric.Count != 2 || metric.Value != "1" || metric.StartedAt != "2026-09-14T00:00:00Z" || metric.FinishedAt != "2026-09-14T00:00:04Z" {
448
+ t.Fatalf("completed observation evidence lost: %+v", metric)
449
+ }
450
+ }