@opensoha/contracts 0.1.4

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 (328) hide show
  1. package/COMPATIBILITY.md +306 -0
  2. package/LICENSE +201 -0
  3. package/README.md +224 -0
  4. package/agent/agent-checkpoint.schema.json +19 -0
  5. package/agent/agent-observation.schema.json +20 -0
  6. package/agent/agent-protocol.schema.json +717 -0
  7. package/agent/agent-provider-definition.schema.json +44 -0
  8. package/agent/agent-provider-registry.schema.json +25 -0
  9. package/agent/agent-provider-runtime-event.schema.json +26 -0
  10. package/agent/agent-run-step.schema.json +24 -0
  11. package/agent/conformance-run.schema.json +19 -0
  12. package/agent/environment-lease.schema.json +31 -0
  13. package/agent/environment-snapshot.schema.json +26 -0
  14. package/agent/environment-template.schema.json +19 -0
  15. package/agent/fleet-target.schema.json +15 -0
  16. package/agent/multi-agent-plan.schema.json +18 -0
  17. package/agent/provider-rollout.schema.json +19 -0
  18. package/apiresponse/response.go +25 -0
  19. package/apiresponse/response_test.go +27 -0
  20. package/auth/token-claims.schema.json +78 -0
  21. package/capabilities/cluster-capability-matrix.schema.json +87 -0
  22. package/compat/json-schema-ecosystem-0.1.0.json +59 -0
  23. package/compat/openapi-0.1.0.snapshot.json +5076 -0
  24. package/compat/openapi-loose-boundaries-0.1.0.json +769 -0
  25. package/connectors/connector-event-envelope.schema.json +64 -0
  26. package/evaluation/evaluation-dataset.schema.json +28 -0
  27. package/evaluation/evaluation-result.schema.json +18 -0
  28. package/evaluation/evaluation-run.schema.json +19 -0
  29. package/evaluation/executor-profile.schema.json +18 -0
  30. package/evaluation/feedback-sample.schema.json +20 -0
  31. package/evaluation/gate-decision.schema.json +20 -0
  32. package/evaluation/gate-policy.schema.json +18 -0
  33. package/evaluation/replay-plan.schema.json +17 -0
  34. package/evaluation/sample-attempt.schema.json +22 -0
  35. package/events/event-envelope.schema.json +58 -0
  36. package/examples/agent-profile/k8s-sre-readonly.json +16 -0
  37. package/examples/cluster-capability-matrix/runtime-parity.json +40 -0
  38. package/examples/connector-event-envelope/feishu-message.json +19 -0
  39. package/examples/gateway-capability-baseline/minimal.json +61 -0
  40. package/examples/gateway-manifest/minimal.json +72 -0
  41. package/examples/governance/approval-request.json +35 -0
  42. package/examples/governance/audit-log.json +14 -0
  43. package/examples/marketplace-catalog/default.json +75 -0
  44. package/examples/mcp-preset/k8s-readonly.json +20 -0
  45. package/examples/openapi/agent-run-tool-call-request.json +11 -0
  46. package/examples/openapi/agent-tool-call-result-envelope.json +19 -0
  47. package/examples/openapi/cloud-extension-point-list-envelope.json +15 -0
  48. package/examples/openapi/directory-runtime-status-envelope.json +31 -0
  49. package/examples/openapi/gateway-manifest-envelope.json +53 -0
  50. package/examples/openapi/tool-invocation-request.json +14 -0
  51. package/examples/openapi/tool-invocation-result-envelope.json +23 -0
  52. package/examples/plugin-manifest/compute-provider.json +27 -0
  53. package/examples/plugin-manifest/connector.json +76 -0
  54. package/examples/plugin-manifest/skill-pack.json +47 -0
  55. package/examples/runner-operation-lifecycle/late-callback.json +27 -0
  56. package/examples/skill-manifest/k8s-sre.json +22 -0
  57. package/examples/token-claims/service-account.json +15 -0
  58. package/fixtures/json-schema/agent-checkpoint/invalid/raw-state.json +1 -0
  59. package/fixtures/json-schema/agent-checkpoint/valid/minimal.json +1 -0
  60. package/fixtures/json-schema/agent-observation/invalid/extra-secret.json +1 -0
  61. package/fixtures/json-schema/agent-observation/valid/tool.json +1 -0
  62. package/fixtures/json-schema/agent-profile/invalid/missing-scope.json +15 -0
  63. package/fixtures/json-schema/agent-profile/valid/k8s-sre-readonly.json +35 -0
  64. package/fixtures/json-schema/agent-protocol/invalid/missing-agent-id.json +3 -0
  65. package/fixtures/json-schema/agent-protocol/valid/agent-run-callback-legacy.json +9 -0
  66. package/fixtures/json-schema/agent-protocol/valid/agent-run-callback-with-events.json +39 -0
  67. package/fixtures/json-schema/agent-protocol/valid/execution-task-claim.json +5 -0
  68. package/fixtures/json-schema/agent-provider-definition/invalid/missing-command.json +1 -0
  69. package/fixtures/json-schema/agent-provider-definition/valid/cli.json +1 -0
  70. package/fixtures/json-schema/agent-provider-registry/invalid/zero-revision.json +1 -0
  71. package/fixtures/json-schema/agent-provider-registry/valid/minimal.json +1 -0
  72. package/fixtures/json-schema/agent-provider-runtime-event/invalid/health-without-provider.json +1 -0
  73. package/fixtures/json-schema/agent-provider-runtime-event/valid/ack.json +1 -0
  74. package/fixtures/json-schema/agent-run-step/invalid/unknown-state.json +1 -0
  75. package/fixtures/json-schema/agent-run-step/valid/running.json +1 -0
  76. package/fixtures/json-schema/approval-request/invalid/missing-status.json +12 -0
  77. package/fixtures/json-schema/approval-request/valid/high-risk-tool.json +56 -0
  78. package/fixtures/json-schema/audit-log/invalid/extra-field.json +10 -0
  79. package/fixtures/json-schema/audit-log/valid/tool-denied.json +23 -0
  80. package/fixtures/json-schema/cluster-capability-matrix/invalid/missing-agent-support.json +16 -0
  81. package/fixtures/json-schema/cluster-capability-matrix/invalid/partial-without-reason.json +18 -0
  82. package/fixtures/json-schema/cluster-capability-matrix/valid/runtime-parity.json +59 -0
  83. package/fixtures/json-schema/conformance-run/invalid/missing.json +1 -0
  84. package/fixtures/json-schema/conformance-run/valid/passed.json +1 -0
  85. package/fixtures/json-schema/connector-checkpoint/invalid/missing.json +1 -0
  86. package/fixtures/json-schema/connector-checkpoint/valid/http.json +1 -0
  87. package/fixtures/json-schema/connector-definition/invalid/missing.json +1 -0
  88. package/fixtures/json-schema/connector-definition/valid/http.json +1 -0
  89. package/fixtures/json-schema/connector-event-envelope/invalid/missing-payload.json +11 -0
  90. package/fixtures/json-schema/connector-event-envelope/invalid/unknown-top-level-field.json +13 -0
  91. package/fixtures/json-schema/connector-event-envelope/valid/feishu-message.json +19 -0
  92. package/fixtures/json-schema/context-envelope/invalid/envelope.json +1 -0
  93. package/fixtures/json-schema/context-envelope/valid/envelope.json +1 -0
  94. package/fixtures/json-schema/environment-lease/invalid/unbounded.json +1 -0
  95. package/fixtures/json-schema/environment-lease/valid/observe.json +1 -0
  96. package/fixtures/json-schema/environment-snapshot/invalid/negative-usage.json +1 -0
  97. package/fixtures/json-schema/environment-snapshot/valid/active.json +1 -0
  98. package/fixtures/json-schema/environment-template/invalid/missing.json +1 -0
  99. package/fixtures/json-schema/environment-template/valid/container.json +1 -0
  100. package/fixtures/json-schema/evaluation-dataset/invalid/no-samples.json +1 -0
  101. package/fixtures/json-schema/evaluation-dataset/valid/minimal.json +1 -0
  102. package/fixtures/json-schema/evaluation-result/invalid/bad-score.json +1 -0
  103. package/fixtures/json-schema/evaluation-result/valid/passed.json +1 -0
  104. package/fixtures/json-schema/evaluation-run/invalid/no-candidate.json +1 -0
  105. package/fixtures/json-schema/evaluation-run/valid/running.json +1 -0
  106. package/fixtures/json-schema/event-envelope/invalid/bad-severity.json +7 -0
  107. package/fixtures/json-schema/event-envelope/valid/minimal.json +14 -0
  108. package/fixtures/json-schema/executor-profile/invalid/missing.json +1 -0
  109. package/fixtures/json-schema/executor-profile/valid/readonly.json +1 -0
  110. package/fixtures/json-schema/feedback-sample/invalid/missing.json +1 -0
  111. package/fixtures/json-schema/feedback-sample/valid/pending.json +1 -0
  112. package/fixtures/json-schema/fleet-target/invalid/missing.json +1 -0
  113. package/fixtures/json-schema/fleet-target/valid/linux.json +1 -0
  114. package/fixtures/json-schema/gate-decision/invalid/missing.json +1 -0
  115. package/fixtures/json-schema/gate-decision/valid/pass.json +1 -0
  116. package/fixtures/json-schema/gate-policy/invalid/missing.json +1 -0
  117. package/fixtures/json-schema/gate-policy/valid/release.json +1 -0
  118. package/fixtures/json-schema/gateway-capability-baseline/invalid/missing-owner.json +32 -0
  119. package/fixtures/json-schema/gateway-capability-baseline/valid/minimal.json +61 -0
  120. package/fixtures/json-schema/gateway-manifest/invalid/bad-risk-level.json +37 -0
  121. package/fixtures/json-schema/gateway-manifest/valid/minimal.json +48 -0
  122. package/fixtures/json-schema/graph-revision/invalid/missing.json +1 -0
  123. package/fixtures/json-schema/graph-revision/valid/active.json +1 -0
  124. package/fixtures/json-schema/index-publish/invalid/missing.json +1 -0
  125. package/fixtures/json-schema/index-publish/valid/publish.json +1 -0
  126. package/fixtures/json-schema/ingestion-job/invalid/missing.json +1 -0
  127. package/fixtures/json-schema/ingestion-job/valid/queued.json +1 -0
  128. package/fixtures/json-schema/ingestion-stage/invalid/missing.json +1 -0
  129. package/fixtures/json-schema/ingestion-stage/valid/succeeded.json +1 -0
  130. package/fixtures/json-schema/knowledge-base/invalid/base.json +1 -0
  131. package/fixtures/json-schema/knowledge-base/valid/base.json +1 -0
  132. package/fixtures/json-schema/knowledge-search/invalid/search.json +1 -0
  133. package/fixtures/json-schema/knowledge-search/valid/search.json +1 -0
  134. package/fixtures/json-schema/knowledge-source/invalid/source.json +1 -0
  135. package/fixtures/json-schema/knowledge-source/valid/source.json +1 -0
  136. package/fixtures/json-schema/marketplace-catalog/invalid/missing-source.json +5 -0
  137. package/fixtures/json-schema/marketplace-catalog/valid/minimal.json +44 -0
  138. package/fixtures/json-schema/mcp-preset/invalid/bad-risk-level.json +20 -0
  139. package/fixtures/json-schema/mcp-preset/valid/k8s-readonly.json +40 -0
  140. package/fixtures/json-schema/memory-policy/invalid/missing.json +1 -0
  141. package/fixtures/json-schema/memory-policy/valid/explicit.json +1 -0
  142. package/fixtures/json-schema/memory-record/invalid/missing.json +1 -0
  143. package/fixtures/json-schema/memory-record/valid/explicit.json +1 -0
  144. package/fixtures/json-schema/multi-agent-plan/invalid/missing.json +1 -0
  145. package/fixtures/json-schema/multi-agent-plan/valid/running.json +1 -0
  146. package/fixtures/json-schema/observability-log-query-v1/invalid/missing-query.json +6 -0
  147. package/fixtures/json-schema/observability-log-query-v1/valid/request.json +15 -0
  148. package/fixtures/json-schema/observability-log-query-v1/valid/response.json +13 -0
  149. package/fixtures/json-schema/plugin-manifest/invalid/bad-type.json +7 -0
  150. package/fixtures/json-schema/plugin-manifest/invalid/logs-query-without-action.json +28 -0
  151. package/fixtures/json-schema/plugin-manifest/invalid/manifest-only-observability-provider.json +26 -0
  152. package/fixtures/json-schema/plugin-manifest/invalid/manifest-only-read-provider.json +23 -0
  153. package/fixtures/json-schema/plugin-manifest/invalid/write-provider-without-actions.json +23 -0
  154. package/fixtures/json-schema/plugin-manifest/valid/compute-provider.json +25 -0
  155. package/fixtures/json-schema/plugin-manifest/valid/minimal.json +14 -0
  156. package/fixtures/json-schema/plugin-manifest/valid/observability-provider.json +29 -0
  157. package/fixtures/json-schema/project-manifest/invalid/inline-secret.json +14 -0
  158. package/fixtures/json-schema/project-manifest/valid/demo.json +21 -0
  159. package/fixtures/json-schema/provider-rollout/invalid/missing.json +1 -0
  160. package/fixtures/json-schema/provider-rollout/valid/canary.json +1 -0
  161. package/fixtures/json-schema/replay-plan/invalid/missing.json +1 -0
  162. package/fixtures/json-schema/replay-plan/valid/readonly.json +1 -0
  163. package/fixtures/json-schema/retrieval-trace/invalid/missing.json +1 -0
  164. package/fixtures/json-schema/retrieval-trace/valid/grounded.json +1 -0
  165. package/fixtures/json-schema/runner-operation-lifecycle/invalid/bad-state.json +7 -0
  166. package/fixtures/json-schema/runner-operation-lifecycle/invalid/missing-runner-id.json +6 -0
  167. package/fixtures/json-schema/runner-operation-lifecycle/valid/final-failed.json +27 -0
  168. package/fixtures/json-schema/runner-operation-lifecycle/valid/heartbeat.json +19 -0
  169. package/fixtures/json-schema/sample-attempt/invalid/missing.json +1 -0
  170. package/fixtures/json-schema/sample-attempt/valid/completed.json +1 -0
  171. package/fixtures/json-schema/skill-manifest/invalid/bad-id.json +5 -0
  172. package/fixtures/json-schema/skill-manifest/valid/minimal.json +8 -0
  173. package/fixtures/json-schema/token-claims/invalid/bad-token-kind.json +9 -0
  174. package/fixtures/json-schema/token-claims/valid/service-account.json +15 -0
  175. package/fixtures/openapi/AIGatewayManifestEnvelope/invalid/bad-date.json +28 -0
  176. package/fixtures/openapi/AIGatewayManifestEnvelope/valid/minimal.json +31 -0
  177. package/fixtures/openapi/AgentProviderRegistryAcknowledgement/invalid/missing-targeted.json +7 -0
  178. package/fixtures/openapi/AgentProviderRegistryAcknowledgement/valid/rollout.json +17 -0
  179. package/fixtures/openapi/AgentRunCallbackRequest/invalid/missing-agent-id.json +6 -0
  180. package/fixtures/openapi/AgentRunCallbackRequest/valid/succeeded.json +10 -0
  181. package/fixtures/openapi/AgentRunCallbackRequest/valid/with-events.json +39 -0
  182. package/fixtures/openapi/AuthResultEnvelope/invalid/missing-tokens.json +13 -0
  183. package/fixtures/openapi/AuthResultEnvelope/valid/session.json +20 -0
  184. package/fixtures/openapi/CloudExtensionPointListEnvelope/invalid/missing-exclusions.json +11 -0
  185. package/fixtures/openapi/CloudExtensionPointListEnvelope/valid/managed-fleet.json +19 -0
  186. package/fixtures/openapi/ComputeAccessSourceListEnvelope/invalid/bad-source-type.json +15 -0
  187. package/fixtures/openapi/ComputeAccessSourceListEnvelope/valid/mixed.json +19 -0
  188. package/fixtures/openapi/ComputeOverviewEnvelope/invalid/bad-section-status.json +9 -0
  189. package/fixtures/openapi/ComputeOverviewEnvelope/valid/partial.json +33 -0
  190. package/fixtures/openapi/ComputeProviderListEnvelope/invalid/bad-domain.json +23 -0
  191. package/fixtures/openapi/ComputeProviderListEnvelope/valid/builtin.json +30 -0
  192. package/fixtures/openapi/ComputeResourceRelationListEnvelope/invalid/bad-relation.json +29 -0
  193. package/fixtures/openapi/ComputeResourceRelationListEnvelope/valid/vm-runtime.json +33 -0
  194. package/fixtures/openapi/ComputeTaskListEnvelope/invalid/missing-source-id.json +19 -0
  195. package/fixtures/openapi/ComputeTaskListEnvelope/valid/running.json +30 -0
  196. package/fixtures/openapi/DeliveryDraftInput/invalid/missing-application-key.json +9 -0
  197. package/fixtures/openapi/DeliveryDraftInput/valid/ai-service.json +25 -0
  198. package/fixtures/openapi/DirectoryEventListEnvelope/invalid/unknown-status.json +13 -0
  199. package/fixtures/openapi/DirectoryEventListEnvelope/valid/failed.json +16 -0
  200. package/fixtures/openapi/DirectoryPersonLifecycle/invalid/unknown-employment-status.json +5 -0
  201. package/fixtures/openapi/DirectoryPersonLifecycle/valid/departed.json +6 -0
  202. package/fixtures/openapi/DirectoryRuntimeStatusEnvelope/invalid/negative-event-count.json +11 -0
  203. package/fixtures/openapi/DirectoryRuntimeStatusEnvelope/valid/verified.json +14 -0
  204. package/fixtures/openapi/DockerContainerStartInput/invalid/missing-image.json +8 -0
  205. package/fixtures/openapi/DockerContainerStartInput/valid/git-dockerfile.json +22 -0
  206. package/fixtures/openapi/DockerOperationCallbackRequest/invalid/logs-not-array.json +7 -0
  207. package/fixtures/openapi/DockerOperationCallbackRequest/valid/succeeded.json +9 -0
  208. package/fixtures/openapi/ErrorEnvelope/invalid/extra-field.json +7 -0
  209. package/fixtures/openapi/ErrorEnvelope/valid/error.json +7 -0
  210. package/fixtures/openapi/EvaluationGateRequest/invalid/missing-candidate.json +4 -0
  211. package/fixtures/openapi/EvaluationGateRequest/valid/candidate.json +5 -0
  212. package/fixtures/openapi/ExecutionCallbackRequest/invalid/extra-field.json +6 -0
  213. package/fixtures/openapi/ExecutionCallbackRequest/valid/succeeded.json +7 -0
  214. package/fixtures/openapi/GovernanceStatusEnvelope/invalid/window-hours-string.json +81 -0
  215. package/fixtures/openapi/GovernanceStatusEnvelope/valid/minimal.json +81 -0
  216. package/fixtures/openapi/IdentityErrorEnvelope/invalid/raw-assertion.json +7 -0
  217. package/fixtures/openapi/IdentityErrorEnvelope/valid/invalid-signature.json +7 -0
  218. package/fixtures/openapi/IdentityOutpostClaimRequest/invalid/missing-protocol-version.json +4 -0
  219. package/fixtures/openapi/IdentityOutpostClaimRequest/valid/current-version.json +5 -0
  220. package/fixtures/openapi/IdentityRuntimeCapabilityEnvelope/invalid/missing-runtime-state.json +17 -0
  221. package/fixtures/openapi/IdentityRuntimeCapabilityEnvelope/valid/available.json +17 -0
  222. package/fixtures/openapi/KnowledgeIngestionJobInput/invalid/missing-source.json +1 -0
  223. package/fixtures/openapi/KnowledgeIngestionJobInput/valid/source.json +3 -0
  224. package/fixtures/openapi/KubernetesResourceAgentCreateRequest/invalid/unresolved-target.json +18 -0
  225. package/fixtures/openapi/KubernetesResourceAgentCreateRequest/valid/resolved-target.json +25 -0
  226. package/fixtures/openapi/KubernetesResourceCreateRequest/invalid/unknown-source.json +5 -0
  227. package/fixtures/openapi/KubernetesResourceCreateRequest/valid/global-multi-document.json +5 -0
  228. package/fixtures/openapi/KubernetesResourceCreateResultEnvelope/invalid/bad-document-index.json +21 -0
  229. package/fixtures/openapi/KubernetesResourceCreateResultEnvelope/valid/partial.json +62 -0
  230. package/fixtures/openapi/KubernetesResourceCreateScopeDecisionEnvelope/invalid/missing-capability.json +12 -0
  231. package/fixtures/openapi/KubernetesResourceCreateScopeDecisionEnvelope/valid/allowed.json +17 -0
  232. package/fixtures/openapi/KubernetesResourcePreflightEnvelope/invalid/warning-without-message.json +32 -0
  233. package/fixtures/openapi/KubernetesResourcePreflightEnvelope/valid/mixed-diagnostics.json +83 -0
  234. package/fixtures/openapi/LogPage/invalid/missing-entry-source.json +12 -0
  235. package/fixtures/openapi/LogPage/valid/partial-runtime.json +42 -0
  236. package/fixtures/openapi/LogQuery/invalid/missing-selector.json +3 -0
  237. package/fixtures/openapi/LogQuery/invalid/unknown-source-mode.json +6 -0
  238. package/fixtures/openapi/LogQuery/valid/docker-project.json +12 -0
  239. package/fixtures/openapi/LogQuery/valid/runtime-workload.json +21 -0
  240. package/fixtures/openapi/LogStreamEvent/invalid/missing-entry.json +3 -0
  241. package/fixtures/openapi/LogStreamEvent/invalid/unknown-event-type.json +3 -0
  242. package/fixtures/openapi/LogStreamEvent/valid/live-entry.json +16 -0
  243. package/fixtures/openapi/MCPCapabilityListEnvelope/invalid/missing-scopes.json +9 -0
  244. package/fixtures/openapi/MCPCapabilityListEnvelope/valid/minimal.json +10 -0
  245. package/fixtures/openapi/MFAAdminResetRequest/invalid/missing-reason.json +3 -0
  246. package/fixtures/openapi/MFAAdminResetRequest/valid/security-incident.json +4 -0
  247. package/fixtures/openapi/MFAWebAuthnRequestOptionsEnvelope/invalid/missing-challenge-id.json +9 -0
  248. package/fixtures/openapi/MFAWebAuthnRequestOptionsEnvelope/valid/step-up.json +11 -0
  249. package/fixtures/openapi/ManifestDeploymentEnvelope/invalid/bad-condition-status.json +33 -0
  250. package/fixtures/openapi/ManifestDeploymentEnvelope/valid/converged.json +36 -0
  251. package/fixtures/openapi/ManifestSourceUpdateInput/invalid/unknown-mode.json +6 -0
  252. package/fixtures/openapi/ManifestSourceUpdateInput/valid/soha-managed.json +6 -0
  253. package/fixtures/openapi/ObservabilityDataSource/valid/healthy.json +33 -0
  254. package/fixtures/openapi/ObservabilityDataSourceInput/invalid/secret-in-config.json +9 -0
  255. package/fixtures/openapi/ObservabilityDataSourceInput/valid/loki.json +33 -0
  256. package/fixtures/openapi/ObservabilityDataSourceInput/valid/provider.json +21 -0
  257. package/fixtures/openapi/PasswordLoginRequest/invalid/extra-field.json +5 -0
  258. package/fixtures/openapi/PasswordLoginRequest/valid/password-login.json +4 -0
  259. package/fixtures/openapi/PersonalAccessTokenInput/invalid/extra-field.json +6 -0
  260. package/fixtures/openapi/PersonalAccessTokenInput/valid/create.json +9 -0
  261. package/fixtures/openapi/PluginManifest/invalid/bad-type.json +7 -0
  262. package/fixtures/openapi/PluginManifest/valid/skill-pack.json +12 -0
  263. package/fixtures/openapi/RepositoryInput/invalid/embedded-token.json +8 -0
  264. package/fixtures/openapi/RepositoryInput/valid/gitlab.json +11 -0
  265. package/fixtures/openapi/RuntimeConfigChangeRequest/invalid/missing-version.json +8 -0
  266. package/fixtures/openapi/RuntimeConfigChangeRequest/valid/feature-flags.json +14 -0
  267. package/fixtures/openapi/RuntimeConfigSnapshotEnvelope/invalid/editable-secret.json +17 -0
  268. package/fixtures/openapi/RuntimeConfigSnapshotEnvelope/invalid/exposed-default-secret.json +18 -0
  269. package/fixtures/openapi/RuntimeConfigSnapshotEnvelope/invalid/exposed-effective-secret.json +18 -0
  270. package/fixtures/openapi/RuntimeConfigSnapshotEnvelope/valid/effective.json +34 -0
  271. package/fixtures/openapi/SAMLMetadataInput/invalid/unknown-source.json +4 -0
  272. package/fixtures/openapi/SAMLMetadataInput/valid/xml.json +4 -0
  273. package/fixtures/openapi/SecretCreateRequest/invalid/missing-value.json +5 -0
  274. package/fixtures/openapi/SecretCreateRequest/valid/create.json +13 -0
  275. package/fixtures/openapi/SecretLeaseRedemptionEnvelope/invalid/invalid-alias.json +9 -0
  276. package/fixtures/openapi/SecretLeaseRedemptionEnvelope/valid/redeem.json +9 -0
  277. package/fixtures/openapi/SecretMetadataEnvelope/invalid/plaintext-value.json +15 -0
  278. package/fixtures/openapi/SecretMetadataEnvelope/valid/metadata.json +20 -0
  279. package/fixtures/openapi/SecretRotateRequest/invalid/empty-value.json +3 -0
  280. package/fixtures/openapi/SecretRotateRequest/valid/rotate.json +3 -0
  281. package/fixtures/openapi/ServiceAccountInput/invalid/missing-status.json +4 -0
  282. package/fixtures/openapi/ServiceAccountInput/valid/create.json +8 -0
  283. package/fixtures/openapi/WorkbenchStreamEvent/invalid/extra-field.json +11 -0
  284. package/fixtures/openapi/WorkbenchStreamEvent/invalid/missing-server-fields.json +5 -0
  285. package/fixtures/openapi/WorkbenchStreamEvent/valid/message-done.json +13 -0
  286. package/gen/go/sohaapi/client.go +989 -0
  287. package/gen/go/sohaapi/client_test.go +2127 -0
  288. package/gen/go/sohaapi/types.go +21450 -0
  289. package/gen/ts/sohaapi/index.ts +22869 -0
  290. package/go.mod +18 -0
  291. package/go.sum +46 -0
  292. package/governance/approval-request.schema.json +187 -0
  293. package/governance/audit-log.schema.json +37 -0
  294. package/helmrelease/release.go +108 -0
  295. package/helmrelease/release_test.go +9 -0
  296. package/knowledge/connector-checkpoint.schema.json +16 -0
  297. package/knowledge/connector-definition.schema.json +18 -0
  298. package/knowledge/context-envelope.schema.json +32 -0
  299. package/knowledge/graph-revision.schema.json +21 -0
  300. package/knowledge/index-publish.schema.json +17 -0
  301. package/knowledge/ingestion-job.schema.json +22 -0
  302. package/knowledge/ingestion-stage.schema.json +19 -0
  303. package/knowledge/knowledge-base.schema.json +48 -0
  304. package/knowledge/knowledge-search.schema.json +21 -0
  305. package/knowledge/knowledge-source.schema.json +28 -0
  306. package/knowledge/retrieval-trace.schema.json +21 -0
  307. package/mcp/gateway-capability-baseline.schema.json +290 -0
  308. package/mcp/gateway-manifest.schema.json +210 -0
  309. package/memory/memory-policy.schema.json +19 -0
  310. package/memory/memory-record.schema.json +25 -0
  311. package/openapi/soha-api.json +42282 -0
  312. package/openapi/soha-api.yaml +21748 -0
  313. package/openapi/spec_generated.go +23 -0
  314. package/openapi/spec_test.go +38 -0
  315. package/package.json +122 -0
  316. package/plugins/marketplace-catalog.schema.json +120 -0
  317. package/plugins/observability-log-query-v1.schema.json +88 -0
  318. package/plugins/plugin-manifest.schema.json +498 -0
  319. package/presets/mcp-preset.schema.json +142 -0
  320. package/profiles/agent-profile.schema.json +154 -0
  321. package/resource/crd_definition.go +10 -0
  322. package/resource/gateway_details_test.go +55 -0
  323. package/resource/models.go +1587 -0
  324. package/resource/models_test.go +72 -0
  325. package/runner/operation-lifecycle.schema.json +112 -0
  326. package/skills/skill-manifest.schema.json +64 -0
  327. package/streamlimit/limited_buffer.go +58 -0
  328. package/streamlimit/limited_buffer_test.go +38 -0
@@ -0,0 +1,2127 @@
1
+ package sohaapi
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "io"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "testing"
10
+ "time"
11
+ )
12
+
13
+ func TestDeliveryPlanStatusLegacyConstants(t *testing.T) {
14
+ statuses := map[DeliveryPlanStatus]string{
15
+ Draft: "draft",
16
+ WaitingApproval: "waiting_approval",
17
+ Confirming: "confirming",
18
+ Confirmed: "confirmed",
19
+ }
20
+ for status, want := range statuses {
21
+ if string(status) != want || !status.Valid() {
22
+ t.Fatalf("DeliveryPlanStatus %q is not the compatible value %q", status, want)
23
+ }
24
+ }
25
+ }
26
+
27
+ func TestObservabilityCredentialKeyLegacyTypes(t *testing.T) {
28
+ dataSource := ObservabilityDataSource{
29
+ CredentialKeys: []ObservabilityDataSourceCredentialKeys{
30
+ ObservabilityDataSourceCredentialKeysBearerToken,
31
+ },
32
+ }
33
+ input := ObservabilityDataSourceInput{
34
+ ClearCredentialKeys: []ObservabilityDataSourceInputClearCredentialKeys{
35
+ ObservabilityDataSourceInputClearCredentialKeysPassword,
36
+ },
37
+ }
38
+ if !dataSource.CredentialKeys[0].Valid() || !input.ClearCredentialKeys[0].Valid() {
39
+ t.Fatal("legacy observability credential key constants must remain valid")
40
+ }
41
+ }
42
+
43
+ func TestSystemHealthAndReadinessUsePublicRequests(t *testing.T) {
44
+ cases := []struct {
45
+ name string
46
+ path string
47
+ call func(*Client) (GenericObject, error)
48
+ }{
49
+ {
50
+ name: "healthz",
51
+ path: "/api/v1/healthz",
52
+ call: func(client *Client) (GenericObject, error) {
53
+ return client.GetHealthz(context.Background())
54
+ },
55
+ },
56
+ {
57
+ name: "readyz",
58
+ path: "/api/v1/readyz",
59
+ call: func(client *Client) (GenericObject, error) {
60
+ return client.GetReadyz(context.Background())
61
+ },
62
+ },
63
+ }
64
+ for _, tt := range cases {
65
+ t.Run(tt.name, func(t *testing.T) {
66
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
67
+ assertPublicRequest(t, r, http.MethodGet, tt.path)
68
+ writeJSON(t, w, map[string]any{"status": "ok", "check": tt.name})
69
+ })
70
+
71
+ result, err := tt.call(client)
72
+ if err != nil {
73
+ t.Fatalf("%s returned error: %v", tt.name, err)
74
+ }
75
+ if got := result["status"]; got != "ok" {
76
+ t.Fatalf("%s status = %v, want ok", tt.name, got)
77
+ }
78
+ })
79
+ }
80
+ }
81
+
82
+ func TestRunnerClaimClientsAcceptNoContent(t *testing.T) {
83
+ cases := []struct {
84
+ name string
85
+ path string
86
+ call func(*Client) (string, error)
87
+ }{
88
+ {
89
+ name: "execution task",
90
+ path: "/api/v1/delivery/execution-tasks/claim",
91
+ call: func(client *Client) (string, error) {
92
+ item, err := client.ClaimExecutionTask(context.Background(), ExecutionTaskClaimRequest{
93
+ AgentID: "agent-1", ProviderKinds: []string{"ci_agent_runner"},
94
+ })
95
+ return item.ID, err
96
+ },
97
+ },
98
+ {
99
+ name: "docker operation",
100
+ path: "/api/v1/docker/operations/claim",
101
+ call: func(client *Client) (string, error) {
102
+ item, err := client.ClaimDockerOperation(context.Background(), DockerOperationClaimRequest{
103
+ AgentID: "agent-1", WorkerID: "worker-1", OperationKinds: []string{"host_sync"},
104
+ })
105
+ return item.ID, err
106
+ },
107
+ },
108
+ }
109
+
110
+ for _, tt := range cases {
111
+ t.Run(tt.name, func(t *testing.T) {
112
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
113
+ assertCommonRequest(t, r, http.MethodPost, tt.path)
114
+ w.WriteHeader(http.StatusNoContent)
115
+ })
116
+
117
+ id, err := tt.call(client)
118
+ if err != nil {
119
+ t.Fatalf("claim returned error: %v", err)
120
+ }
121
+ if id != "" {
122
+ t.Fatalf("claim ID = %q, want empty", id)
123
+ }
124
+ })
125
+ }
126
+ }
127
+
128
+ func TestRedeemSecretLeaseSendsRunnerBindingHeaders(t *testing.T) {
129
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
130
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/runner/secret-leases/lease%2Fone/redeem")
131
+ if got := r.Header.Get("X-Soha-Secret-Lease-Token"); got != "one-time-token" {
132
+ t.Fatalf("lease token = %q", got)
133
+ }
134
+ if got := r.Header.Get("X-Soha-Agent-ID"); got != "agent-1" {
135
+ t.Fatalf("agent id = %q", got)
136
+ }
137
+ writeJSON(t, w, map[string]any{"data": map[string]any{
138
+ "leaseId": "lease/one", "expiresAt": time.Now().UTC().Add(time.Minute), "values": map[string]string{"REGISTRY_TOKEN": "secret-value"},
139
+ }})
140
+ })
141
+
142
+ result, err := client.RedeemSecretLease(context.Background(), "lease/one", RedeemSecretLeaseParams{
143
+ XSohaSecretLeaseToken: "one-time-token",
144
+ XSohaAgentID: "agent-1",
145
+ })
146
+ if err != nil {
147
+ t.Fatal(err)
148
+ }
149
+ if result.Values["REGISTRY_TOKEN"] != "secret-value" {
150
+ t.Fatalf("redeemed value = %q", result.Values["REGISTRY_TOKEN"])
151
+ }
152
+ }
153
+
154
+ func TestIdentityOutpostRuntimeClientSurface(t *testing.T) {
155
+ now := time.Date(2026, 7, 27, 10, 0, 0, 0, time.UTC)
156
+ requests := 0
157
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
158
+ requests++
159
+ switch requests {
160
+ case 1:
161
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/outposts/runtime/claim")
162
+ body := decodeJSONRequestBody[IdentityOutpostClaimRequest](t, r)
163
+ if body.AgentID != "agent/one" || body.CurrentConfigurationVersion != 7 {
164
+ t.Fatalf("unexpected claim request %#v", body)
165
+ }
166
+ writeJSON(t, w, map[string]any{"data": map[string]any{
167
+ "outpostId": "outpost/one", "protocolVersion": "1", "configurationVersion": 8,
168
+ "issuedAt": now, "expiresAt": now.Add(time.Minute), "keyId": "key-1", "checkUrl": "https://soha.example/api/v1/identity/outposts/outpost%2Fone/check",
169
+ "routes": []any{}, "signature": "signature",
170
+ }})
171
+ case 2:
172
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/outposts/outpost%2Fone/heartbeat")
173
+ body := decodeJSONRequestBody[IdentityOutpostHeartbeatRequest](t, r)
174
+ if body.AgentID != "agent/one" || body.ConfigurationVersion != 8 {
175
+ t.Fatalf("unexpected heartbeat request %#v", body)
176
+ }
177
+ writeJSON(t, w, map[string]any{"data": map[string]any{"accepted": true, "desiredConfigurationVersion": 8}})
178
+ case 3:
179
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/outposts/outpost%2Fone/check")
180
+ body := decodeJSONRequestBody[IdentityOutpostAccessCheckRequest](t, r)
181
+ if body.RequestPath != "/admin" || body.ConfigurationVersion != 8 {
182
+ t.Fatalf("unexpected access check request %#v", body)
183
+ }
184
+ writeJSON(t, w, map[string]any{"data": map[string]any{"decision": "allow", "statusCode": 200}})
185
+ case 4:
186
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/outposts/outpost%2Fone/events")
187
+ body := decodeJSONRequestBody[IdentityOutpostEventBatchRequest](t, r)
188
+ if body.AgentID != "agent/one" || len(body.Events) != 1 {
189
+ t.Fatalf("unexpected event batch %#v", body)
190
+ }
191
+ writeJSON(t, w, map[string]any{"status": "accepted"})
192
+ default:
193
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
194
+ }
195
+ })
196
+
197
+ config, err := client.ClaimIdentityOutpostConfig(context.Background(), IdentityOutpostClaimRequest{
198
+ AgentID: "agent/one", SupportedProtocolVersion: "1", CurrentConfigurationVersion: 7,
199
+ })
200
+ if err != nil || config == nil || config.ConfigurationVersion != 8 {
201
+ t.Fatalf("ClaimIdentityOutpostConfig = %#v, %v", config, err)
202
+ }
203
+ heartbeat, err := client.HeartbeatIdentityOutpost(context.Background(), "outpost/one", IdentityOutpostHeartbeatRequest{
204
+ AgentID: "agent/one", ConfigurationVersion: 8, Status: IdentityOutpostHeartbeatRequestStatusHealthy, CheckedAt: now,
205
+ })
206
+ if err != nil || !heartbeat.Accepted {
207
+ t.Fatalf("HeartbeatIdentityOutpost = %#v, %v", heartbeat, err)
208
+ }
209
+ decision, err := client.CheckIdentityOutpostAccess(context.Background(), "outpost/one", IdentityOutpostAccessCheckRequest{
210
+ ConfigurationVersion: 8, ProviderID: "provider-1", OriginalURL: "https://app.example/admin",
211
+ Method: http.MethodGet, RequestHost: "app.example", RequestPath: "/admin",
212
+ })
213
+ if err != nil || decision.Decision != IdentityOutpostAccessCheckDecisionAllow {
214
+ t.Fatalf("CheckIdentityOutpostAccess = %#v, %v", decision, err)
215
+ }
216
+ status, err := client.AppendIdentityOutpostEvents(context.Background(), "outpost/one", IdentityOutpostEventBatchRequest{
217
+ AgentID: "agent/one",
218
+ Events: []IdentityOutpostRuntimeEvent{{
219
+ ID: "event-1", Type: ConfigurationApplied, ConfigurationVersion: 8, OccurredAt: now,
220
+ }},
221
+ })
222
+ if err != nil || status.Status != "accepted" {
223
+ t.Fatalf("AppendIdentityOutpostEvents = %#v, %v", status, err)
224
+ }
225
+ }
226
+
227
+ func TestIdentityMFAClientSurface(t *testing.T) {
228
+ now := time.Date(2026, 7, 27, 11, 0, 0, 0, time.UTC)
229
+ requests := 0
230
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
231
+ requests++
232
+ switch requests {
233
+ case 1:
234
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/mfa/webauthn/authenticate")
235
+ body := decodeJSONRequestBody[MFAWebAuthnAuthenticationRequest](t, r)
236
+ if body.Purpose != MFAWebAuthnPurposeStepUp || body.ApplicationID != "app/one" {
237
+ t.Fatalf("unexpected WebAuthn authentication request %#v", body)
238
+ }
239
+ writeJSON(t, w, map[string]any{"data": map[string]any{
240
+ "challengeId": "challenge-1", "challenge": "challenge", "rpId": "soha.example.com",
241
+ "timeoutMilliseconds": 60000, "userVerification": "required", "expiresAt": now.Add(time.Minute),
242
+ }})
243
+ case 2:
244
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/users/user%2Fone/mfa/credentials/credential%2Fone/revoke")
245
+ writeJSON(t, w, map[string]any{"status": "revoked"})
246
+ case 3:
247
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/identity/users/user%2Fone/mfa/reset")
248
+ body := decodeJSONRequestBody[MFAAdminResetRequest](t, r)
249
+ if body.Reason != "security incident" || !body.RevokeSessions {
250
+ t.Fatalf("unexpected MFA reset request %#v", body)
251
+ }
252
+ writeJSON(t, w, map[string]any{"data": map[string]any{
253
+ "userId": "user/one", "revokedCredentialCount": 2, "recoveryCodesRevoked": 8,
254
+ "sessionsRevoked": 3, "resetAt": now,
255
+ }})
256
+ default:
257
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
258
+ }
259
+ })
260
+
261
+ options, err := client.BeginWebAuthnAuthentication(context.Background(), MFAWebAuthnAuthenticationRequest{
262
+ Purpose: MFAWebAuthnPurposeStepUp, ApplicationID: "app/one",
263
+ })
264
+ if err != nil || options.ChallengeID != "challenge-1" || options.UserVerification != MFAWebAuthnUserVerificationRequired {
265
+ t.Fatalf("BeginWebAuthnAuthentication = %#v, %v", options, err)
266
+ }
267
+ status, err := client.AdminRevokeUserMFACredential(context.Background(), "user/one", "credential/one")
268
+ if err != nil || status.Status != "revoked" {
269
+ t.Fatalf("AdminRevokeUserMFACredential = %#v, %v", status, err)
270
+ }
271
+ reset, err := client.AdminResetUserMFA(context.Background(), "user/one", MFAAdminResetRequest{
272
+ Reason: "security incident", RevokeSessions: true,
273
+ })
274
+ if err != nil || reset.RevokedCredentialCount != 2 || reset.SessionsRevoked != 3 {
275
+ t.Fatalf("AdminResetUserMFA = %#v, %v", reset, err)
276
+ }
277
+ }
278
+
279
+ func TestComputeTaskCenterClientSurface(t *testing.T) {
280
+ requests := 0
281
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
282
+ requests++
283
+ switch requests {
284
+ case 1:
285
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/compute/tasks")
286
+ assertQuery(t, r, map[string]string{
287
+ "domain": "virtualization", "resourceKind": "cluster", "resourceId": "cluster-1", "limit": "1",
288
+ })
289
+ writeJSON(t, w, map[string]any{"items": []any{}})
290
+ case 2:
291
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/compute/tasks/virtualization/task%2Fone")
292
+ writeJSON(t, w, computeTaskEnvelopeFixture("task/one"))
293
+ case 3:
294
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/compute/tasks/virtualization/task%2Fone/logs")
295
+ writeJSON(t, w, map[string]any{"items": []map[string]any{{
296
+ "id": "log-1", "taskId": "task/one", "logLevel": "info", "message": "started", "createdAt": "2026-07-16T08:00:00Z",
297
+ }}})
298
+ case 4:
299
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/compute/tasks/virtualization/task%2Fone/cancel")
300
+ if body := decodeJSONRequestBody[ComputeTaskMutationRequest](t, r); body.Reason != "operator request" {
301
+ t.Fatalf("cancel reason = %q", body.Reason)
302
+ }
303
+ writeJSON(t, w, computeTaskEnvelopeFixture("task/one"))
304
+ case 5:
305
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/compute/tasks/virtualization/task%2Fone/retry")
306
+ writeJSON(t, w, computeTaskEnvelopeFixture("task/two"))
307
+ default:
308
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
309
+ }
310
+ })
311
+
312
+ items, err := client.ListComputeTasks(context.Background(), ListComputeTasksParams{
313
+ Domain: ComputeTaskDomainVirtualization, ResourceKind: "cluster", ResourceID: "cluster-1", Limit: 1,
314
+ })
315
+ if err != nil || len(items.Items) != 0 {
316
+ t.Fatalf("ListComputeTasks = %#v, %v", items, err)
317
+ }
318
+ if task, err := client.GetComputeTask(context.Background(), ComputeTaskDomainVirtualization, "task/one"); err != nil || task.ID != "task/one" {
319
+ t.Fatalf("GetComputeTask = %#v, %v", task, err)
320
+ }
321
+ if logs, err := client.ListComputeTaskLogs(context.Background(), ComputeTaskDomainVirtualization, "task/one"); err != nil || len(logs) != 1 {
322
+ t.Fatalf("ListComputeTaskLogs = %#v, %v", logs, err)
323
+ }
324
+ if _, err := client.CancelComputeTask(context.Background(), ComputeTaskDomainVirtualization, "task/one", ComputeTaskMutationRequest{Reason: "operator request"}); err != nil {
325
+ t.Fatalf("CancelComputeTask returned error: %v", err)
326
+ }
327
+ if task, err := client.RetryComputeTask(context.Background(), ComputeTaskDomainVirtualization, "task/one", ComputeTaskMutationRequest{}); err != nil || task.ID != "task/two" {
328
+ t.Fatalf("RetryComputeTask = %#v, %v", task, err)
329
+ }
330
+ }
331
+
332
+ func TestResourceLogClientSurface(t *testing.T) {
333
+ requests := []string{
334
+ "/api/v1/clusters/cluster%2Fone/logs/query",
335
+ "/api/v1/docker/projects/project%2Fone/logs/query",
336
+ "/api/v1/delivery/applications/app%2Fone/environments/env%2Fone/logs/query",
337
+ }
338
+ requestIndex := 0
339
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
340
+ assertCommonRequest(t, r, http.MethodPost, requests[requestIndex])
341
+ query := decodeJSONRequestBody[LogQuery](t, r)
342
+ if query.Selector == nil || query.Selector.Namespace != "default" {
343
+ t.Fatalf("log query selector = %#v", query.Selector)
344
+ }
345
+ writeJSON(t, w, map[string]any{"data": map[string]any{
346
+ "entries": []any{}, "partial": false, "truncated": false, "scopeRestricted": false,
347
+ }})
348
+ requestIndex++
349
+ })
350
+
351
+ query := LogQuery{Selector: &LogSourceSelector{Namespace: "default"}}
352
+ if _, err := client.QueryClusterLogs(context.Background(), "cluster/one", query); err != nil {
353
+ t.Fatal(err)
354
+ }
355
+ if _, err := client.QueryDockerProjectLogs(context.Background(), "project/one", query); err != nil {
356
+ t.Fatal(err)
357
+ }
358
+ if _, err := client.QueryDeliveryEnvironmentLogs(context.Background(), "app/one", "env/one", query); err != nil {
359
+ t.Fatal(err)
360
+ }
361
+ if requestIndex != len(requests) {
362
+ t.Fatalf("requests = %d, want %d", requestIndex, len(requests))
363
+ }
364
+ }
365
+
366
+ func computeTaskEnvelopeFixture(id string) map[string]any {
367
+ return map[string]any{"data": map[string]any{
368
+ "id": id, "domain": "virtualization", "sourceType": "operation", "sourceId": id,
369
+ "kind": "sync", "category": "sync", "normalizedStatus": "running", "rawStatus": "running",
370
+ "resources": []any{}, "attemptCount": 1, "cancelable": true, "retryable": false,
371
+ "availableActions": []string{"logs", "cancel"}, "createdAt": "2026-07-16T08:00:00Z",
372
+ }}
373
+ }
374
+
375
+ func TestListAuthProvidersBuildsPublicRequestAndDecodesItems(t *testing.T) {
376
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
377
+ assertPublicRequest(t, r, http.MethodGet, "/api/v1/auth/providers")
378
+ assertQuery(t, r, map[string]string{})
379
+ writeJSON(t, w, map[string]any{
380
+ "items": []map[string]any{
381
+ {
382
+ "id": "local",
383
+ "name": "Password",
384
+ "type": "password",
385
+ "enabled": true,
386
+ "loginUrl": "/auth/login",
387
+ },
388
+ },
389
+ })
390
+ })
391
+
392
+ items, err := client.ListAuthProviders(context.Background())
393
+ if err != nil {
394
+ t.Fatalf("ListAuthProviders returned error: %v", err)
395
+ }
396
+ if len(items) != 1 {
397
+ t.Fatalf("got %d auth providers, want 1", len(items))
398
+ }
399
+ if got := items[0].ID; got != "local" {
400
+ t.Fatalf("auth provider ID = %q, want local", got)
401
+ }
402
+ if !items[0].Enabled {
403
+ t.Fatal("auth provider Enabled = false, want true")
404
+ }
405
+ }
406
+
407
+ func TestKnowledgeClientSearchAndListPaths(t *testing.T) {
408
+ requests := 0
409
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
410
+ requests++
411
+ switch requests {
412
+ case 1:
413
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/knowledge-bases/base%2Fone/documents")
414
+ assertQuery(t, r, map[string]string{"limit": "25"})
415
+ writeJSON(t, w, map[string]any{"items": []any{}})
416
+ case 2:
417
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai/knowledge/search")
418
+ body := decodeJSONRequestBody[KnowledgeSearchRequest](t, r)
419
+ if body.Query != "rollback" || len(body.KnowledgeBaseIDs) != 1 || body.KnowledgeBaseIDs[0] != "base/one" {
420
+ t.Fatalf("unexpected knowledge search body %#v", body)
421
+ }
422
+ writeJSON(t, w, map[string]any{"data": map[string]any{
423
+ "query": "rollback", "hits": []any{}, "citations": []any{}, "candidateCount": 0,
424
+ "timingMs": 2, "noAnswer": true, "traceId": "trace-1",
425
+ }})
426
+ default:
427
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
428
+ }
429
+ })
430
+
431
+ items, err := client.ListKnowledgeDocuments(context.Background(), "base/one", 25)
432
+ if err != nil || len(items) != 0 {
433
+ t.Fatalf("ListKnowledgeDocuments = %#v, %v", items, err)
434
+ }
435
+ result, err := client.SearchKnowledge(context.Background(), KnowledgeSearchRequest{
436
+ KnowledgeBaseIDs: []string{"base/one"}, Query: "rollback", TopK: 5,
437
+ })
438
+ if err != nil {
439
+ t.Fatalf("SearchKnowledge returned error: %v", err)
440
+ }
441
+ if !result.NoAnswer || result.TraceID != "trace-1" {
442
+ t.Fatalf("unexpected knowledge result %#v", result)
443
+ }
444
+ }
445
+
446
+ func TestAgentProviderControlPlaneClientSurface(t *testing.T) {
447
+ observedAt := time.Date(2026, 7, 14, 9, 0, 0, 0, time.UTC)
448
+ catalog := AgentProviderCatalog{
449
+ SchemaVersion: "opensoha.dev/agent-provider-catalog/v1",
450
+ Revision: 3,
451
+ Digest: "sha256:catalog",
452
+ CreatedAt: observedAt,
453
+ Providers: []AgentProviderDefinition{},
454
+ }
455
+ ack := AgentProviderRegistryAcknowledgement{
456
+ RunnerID: "runner/one", Revision: 3, ActiveRevision: 3, Accepted: true, ObservedAt: observedAt,
457
+ }
458
+ snapshot := AgentProviderRegistrySnapshot{
459
+ SchemaVersion: "opensoha.dev/agent-provider-registry/v1",
460
+ Revision: 3,
461
+ Digest: "sha256:snapshot",
462
+ IssuedAt: observedAt,
463
+ Providers: []AgentProviderDefinition{},
464
+ }
465
+
466
+ requests := 0
467
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
468
+ requests++
469
+ switch requests {
470
+ case 1:
471
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/agent-providers/catalog")
472
+ writeJSON(t, w, map[string]any{"data": catalog})
473
+ case 2:
474
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/agent-providers/runtime-status")
475
+ writeJSON(t, w, map[string]any{"data": AgentProviderRuntimeStatus{
476
+ CatalogRevision: 3, CatalogDigest: catalog.Digest, RunnerCount: 1,
477
+ Acknowledgements: []AgentProviderRegistryAcknowledgement{ack},
478
+ }})
479
+ case 3:
480
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/agent-providers/registry-snapshot")
481
+ assertQuery(t, r, map[string]string{"runnerId": "runner/one"})
482
+ writeJSON(t, w, map[string]any{"data": snapshot})
483
+ case 4:
484
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai/agent-providers/registry-acks")
485
+ body := decodeJSONRequestBody[AgentProviderRegistryAcknowledgement](t, r)
486
+ if body.RunnerID != ack.RunnerID || body.Revision != ack.Revision || !body.Accepted {
487
+ t.Fatalf("unexpected registry acknowledgement %#v", body)
488
+ }
489
+ writeJSON(t, w, map[string]any{"data": ack})
490
+ default:
491
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
492
+ }
493
+ })
494
+
495
+ if item, err := client.GetAgentProviderCatalog(context.Background()); err != nil || item.Revision != 3 {
496
+ t.Fatalf("GetAgentProviderCatalog = %#v, %v", item, err)
497
+ }
498
+ if item, err := client.GetAgentProviderRuntimeStatus(context.Background()); err != nil || item.RunnerCount != 1 {
499
+ t.Fatalf("GetAgentProviderRuntimeStatus = %#v, %v", item, err)
500
+ }
501
+ if item, err := client.GetAgentProviderRegistrySnapshot(context.Background(), ack.RunnerID); err != nil || item.Revision != 3 {
502
+ t.Fatalf("GetAgentProviderRegistrySnapshot = %#v, %v", item, err)
503
+ }
504
+ if item, err := client.AcknowledgeAgentProviderRegistry(context.Background(), ack); err != nil || item.RunnerID != ack.RunnerID {
505
+ t.Fatalf("AcknowledgeAgentProviderRegistry = %#v, %v", item, err)
506
+ }
507
+ }
508
+
509
+ func TestEvaluationClientSurface(t *testing.T) {
510
+ createdAt := time.Date(2026, 7, 14, 8, 0, 0, 0, time.UTC)
511
+ dataset := EvaluationDataset{
512
+ SchemaVersion: "opensoha.dev/evaluation-dataset/v1",
513
+ ID: "rag-regression",
514
+ Name: "RAG regression",
515
+ Version: "v1",
516
+ Samples: []EvaluationDatasetSample{{ID: "sample-1", Input: "What failed?"}},
517
+ CreatedAt: createdAt,
518
+ }
519
+ run := EvaluationRun{
520
+ SchemaVersion: "opensoha.dev/evaluation-run/v1",
521
+ ID: "run/1",
522
+ DatasetID: dataset.ID,
523
+ DatasetVersion: dataset.Version,
524
+ CandidateRefs: map[string]string{"prompt": "prompt:v2"},
525
+ Status: EvaluationRunStatus("running"),
526
+ StartedAt: createdAt,
527
+ }
528
+
529
+ requests := 0
530
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
531
+ requests++
532
+ switch requests {
533
+ case 1:
534
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/evaluations/datasets")
535
+ writeJSON(t, w, map[string]any{"items": []EvaluationDataset{dataset}})
536
+ case 2:
537
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai/evaluations/datasets")
538
+ if body := decodeJSONRequestBody[EvaluationDataset](t, r); body.ID != dataset.ID {
539
+ t.Fatalf("dataset id = %q", body.ID)
540
+ }
541
+ writeJSON(t, w, map[string]any{"data": dataset})
542
+ case 3:
543
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/evaluations/runs")
544
+ writeJSON(t, w, map[string]any{"items": []EvaluationRun{run}})
545
+ case 4:
546
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai/evaluations/runs")
547
+ if body := decodeJSONRequestBody[EvaluationRun](t, r); body.CandidateRefs["prompt"] != "prompt:v2" {
548
+ t.Fatalf("candidate refs = %#v", body.CandidateRefs)
549
+ }
550
+ writeJSON(t, w, map[string]any{"data": run})
551
+ case 5:
552
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/evaluations/runs/run%2F1")
553
+ writeJSON(t, w, map[string]any{"data": run})
554
+ case 6:
555
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai/evaluations/runs/run%2F1/results")
556
+ writeJSON(t, w, map[string]any{"items": []EvaluationResult{{
557
+ SchemaVersion: "opensoha.dev/evaluation-result/v1", SampleID: "sample-1",
558
+ Scores: map[string]float32{"fact_recall": 1}, Passed: true,
559
+ }}})
560
+ case 7:
561
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai/evaluations/runs/run%2F1/complete")
562
+ body := decodeJSONRequestBody[EvaluationCompleteRunInput](t, r)
563
+ if len(body.Outputs) != 1 || body.Outputs[0].SampleID != "sample-1" {
564
+ t.Fatalf("completion input = %#v", body)
565
+ }
566
+ completed := run
567
+ completed.Status = EvaluationRunStatus("completed")
568
+ completed.AggregateScores = map[string]float32{"fact_recall": 1}
569
+ writeJSON(t, w, map[string]any{"data": completed})
570
+ default:
571
+ t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
572
+ }
573
+ })
574
+
575
+ if items, err := client.ListEvaluationDatasets(context.Background()); err != nil || len(items) != 1 {
576
+ t.Fatalf("ListEvaluationDatasets = %#v, %v", items, err)
577
+ }
578
+ if item, err := client.CreateEvaluationDataset(context.Background(), dataset); err != nil || item.ID != dataset.ID {
579
+ t.Fatalf("CreateEvaluationDataset = %#v, %v", item, err)
580
+ }
581
+ if items, err := client.ListEvaluationRuns(context.Background()); err != nil || len(items) != 1 {
582
+ t.Fatalf("ListEvaluationRuns = %#v, %v", items, err)
583
+ }
584
+ if item, err := client.StartEvaluationRun(context.Background(), run); err != nil || item.ID != run.ID {
585
+ t.Fatalf("StartEvaluationRun = %#v, %v", item, err)
586
+ }
587
+ if item, err := client.GetEvaluationRun(context.Background(), run.ID); err != nil || item.ID != run.ID {
588
+ t.Fatalf("GetEvaluationRun = %#v, %v", item, err)
589
+ }
590
+ if items, err := client.ListEvaluationRunResults(context.Background(), run.ID); err != nil || len(items) != 1 || !items[0].Passed {
591
+ t.Fatalf("ListEvaluationRunResults = %#v, %v", items, err)
592
+ }
593
+ completed, err := client.CompleteEvaluationRun(context.Background(), run.ID, EvaluationCompleteRunInput{
594
+ Outputs: []EvaluationSampleOutput{{SampleID: "sample-1", ProducedFacts: []string{"fact"}}},
595
+ })
596
+ if err != nil || completed.Status != EvaluationRunStatus("completed") {
597
+ t.Fatalf("CompleteEvaluationRun = %#v, %v", completed, err)
598
+ }
599
+ }
600
+
601
+ func TestGetAuthLoginOptionsBuildsPublicRequestAndDecodesData(t *testing.T) {
602
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
603
+ assertPublicRequest(t, r, http.MethodGet, "/api/v1/auth/login-options")
604
+ writeJSON(t, w, map[string]any{
605
+ "data": map[string]any{
606
+ "verification": map[string]any{"sliderEnabled": true},
607
+ },
608
+ })
609
+ })
610
+
611
+ options, err := client.GetAuthLoginOptions(context.Background())
612
+ if err != nil {
613
+ t.Fatalf("GetAuthLoginOptions returned error: %v", err)
614
+ }
615
+ if !options.Verification.SliderEnabled {
616
+ t.Fatal("login options slider enabled = false, want true")
617
+ }
618
+ }
619
+
620
+ func TestPasswordLoginPostsPublicBodyAndDecodesData(t *testing.T) {
621
+ expiresAt := time.Date(2026, 6, 9, 8, 0, 0, 0, time.UTC)
622
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
623
+ assertPublicRequest(t, r, http.MethodPost, "/api/v1/auth/login")
624
+ body := decodeJSONRequestBody[PasswordLoginRequest](t, r)
625
+ if got := body.Login; got != "admin" {
626
+ t.Fatalf("password login login = %q, want admin", got)
627
+ }
628
+ if got := body.Password; got != "secret" {
629
+ t.Fatalf("password login password = %q, want secret", got)
630
+ }
631
+ writeJSON(t, w, authResultJSON(expiresAt, "access-login"))
632
+ })
633
+
634
+ result, err := client.PasswordLogin(context.Background(), PasswordLoginRequest{
635
+ Login: "admin",
636
+ Password: "secret",
637
+ })
638
+ if err != nil {
639
+ t.Fatalf("PasswordLogin returned error: %v", err)
640
+ }
641
+ if got := result.Tokens.AccessToken; got != "access-login" {
642
+ t.Fatalf("password login access token = %q, want access-login", got)
643
+ }
644
+ if got := result.User.UserID; got != "user-1" {
645
+ t.Fatalf("password login user ID = %q, want user-1", got)
646
+ }
647
+ }
648
+
649
+ func TestRefreshAuthSessionPostsPublicOptionalBodyAndDecodesData(t *testing.T) {
650
+ expiresAt := time.Date(2026, 6, 9, 9, 0, 0, 0, time.UTC)
651
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
652
+ assertPublicRequest(t, r, http.MethodPost, "/api/v1/auth/refresh")
653
+ body := decodeJSONRequestBody[RefreshRequest](t, r)
654
+ if got := body.RefreshToken; got != "refresh-1" {
655
+ t.Fatalf("refresh token = %q, want refresh-1", got)
656
+ }
657
+ writeJSON(t, w, authResultJSON(expiresAt, "access-refresh"))
658
+ })
659
+
660
+ result, err := client.RefreshAuthSession(context.Background(), &RefreshRequest{RefreshToken: "refresh-1"})
661
+ if err != nil {
662
+ t.Fatalf("RefreshAuthSession returned error: %v", err)
663
+ }
664
+ if got := result.Tokens.AccessToken; got != "access-refresh" {
665
+ t.Fatalf("refresh access token = %q, want access-refresh", got)
666
+ }
667
+ if got := result.Tokens.ExpiresAt; !got.Equal(expiresAt) {
668
+ t.Fatalf("refresh ExpiresAt = %s, want %s", got, expiresAt)
669
+ }
670
+ }
671
+
672
+ func TestExchangeOIDCCodePostsPublicBodyAndDecodesData(t *testing.T) {
673
+ expiresAt := time.Date(2026, 6, 9, 10, 0, 0, 0, time.UTC)
674
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
675
+ assertPublicRequest(t, r, http.MethodPost, "/api/v1/auth/oidc/exchange")
676
+ body := decodeJSONRequestBody[OIDCExchangeRequest](t, r)
677
+ if got := body.Code; got != "oidc-code" {
678
+ t.Fatalf("OIDC exchange code = %q, want oidc-code", got)
679
+ }
680
+ writeJSON(t, w, authResultJSON(expiresAt, "access-oidc"))
681
+ })
682
+
683
+ result, err := client.ExchangeOIDCCode(context.Background(), OIDCExchangeRequest{Code: "oidc-code"})
684
+ if err != nil {
685
+ t.Fatalf("ExchangeOIDCCode returned error: %v", err)
686
+ }
687
+ if got := result.Tokens.AccessToken; got != "access-oidc" {
688
+ t.Fatalf("OIDC exchange access token = %q, want access-oidc", got)
689
+ }
690
+ }
691
+
692
+ func TestCurrentAuthMethodsBuildBearerRequestsAndDecodeData(t *testing.T) {
693
+ lastLoginAt := time.Date(2026, 6, 9, 11, 0, 0, 0, time.UTC)
694
+ cases := []struct {
695
+ name string
696
+ path string
697
+ call func(*testing.T, *Client) error
698
+ body map[string]any
699
+ }{
700
+ {
701
+ name: "me",
702
+ path: "/api/v1/auth/me",
703
+ call: func(t *testing.T, client *Client) error {
704
+ t.Helper()
705
+ principal, err := client.GetCurrentPrincipal(context.Background())
706
+ if err != nil {
707
+ return err
708
+ }
709
+ if got := principal.UserID; got != "user-1" {
710
+ t.Fatalf("current principal user ID = %q, want user-1", got)
711
+ }
712
+ return nil
713
+ },
714
+ body: map[string]any{"data": principalJSON("user-1")},
715
+ },
716
+ {
717
+ name: "profile",
718
+ path: "/api/v1/auth/profile",
719
+ call: func(t *testing.T, client *Client) error {
720
+ t.Helper()
721
+ profile, err := client.GetCurrentUserProfile(context.Background())
722
+ if err != nil {
723
+ return err
724
+ }
725
+ if got := profile.Username; got != "admin" {
726
+ t.Fatalf("current user profile username = %q, want admin", got)
727
+ }
728
+ if profile.LastLoginAt == nil || !profile.LastLoginAt.Equal(lastLoginAt) {
729
+ t.Fatalf("current user profile LastLoginAt = %v, want %s", profile.LastLoginAt, lastLoginAt)
730
+ }
731
+ return nil
732
+ },
733
+ body: map[string]any{
734
+ "data": map[string]any{
735
+ "userId": "user-1",
736
+ "username": "admin",
737
+ "displayName": "Admin User",
738
+ "email": "admin@example.com",
739
+ "roles": []string{"admin"},
740
+ "teams": []string{"platform"},
741
+ "projects": []string{"default"},
742
+ "tags": []string{"ops"},
743
+ "status": "active",
744
+ "lastLoginAt": lastLoginAt,
745
+ "sessions": []map[string]any{{"id": "session-1"}},
746
+ },
747
+ },
748
+ },
749
+ {
750
+ name: "bootstrap",
751
+ path: "/api/v1/auth/bootstrap",
752
+ call: func(t *testing.T, client *Client) error {
753
+ t.Helper()
754
+ bootstrap, err := client.GetAuthBootstrap(context.Background())
755
+ if err != nil {
756
+ return err
757
+ }
758
+ if got := bootstrap.CurrentUser.UserID; got != "user-1" {
759
+ t.Fatalf("auth bootstrap current user ID = %q, want user-1", got)
760
+ }
761
+ if got := bootstrap.Branding["productName"]; got != "OpenSoha" {
762
+ t.Fatalf("auth bootstrap branding product name = %v, want OpenSoha", got)
763
+ }
764
+ return nil
765
+ },
766
+ body: map[string]any{
767
+ "data": map[string]any{
768
+ "branding": map[string]any{"productName": "OpenSoha"},
769
+ "currentUser": principalJSON("user-1"),
770
+ "permissionSnapshot": map[string]any{"allowed": []string{"auth.read"}},
771
+ "user": principalJSON("user-1"),
772
+ },
773
+ },
774
+ },
775
+ }
776
+ for _, tt := range cases {
777
+ t.Run(tt.name, func(t *testing.T) {
778
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
779
+ assertCommonRequest(t, r, http.MethodGet, tt.path)
780
+ writeJSON(t, w, tt.body)
781
+ })
782
+
783
+ if err := tt.call(t, client); err != nil {
784
+ t.Fatalf("%s returned error: %v", tt.name, err)
785
+ }
786
+ })
787
+ }
788
+ }
789
+
790
+ func TestLogoutAuthSessionPostsBearerBodyAndAcceptsNoContent(t *testing.T) {
791
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
792
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/auth/logout")
793
+ body := decodeJSONRequestBody[RefreshRequest](t, r)
794
+ if got := body.RefreshToken; got != "refresh-logout" {
795
+ t.Fatalf("logout refresh token = %q, want refresh-logout", got)
796
+ }
797
+ w.WriteHeader(http.StatusNoContent)
798
+ })
799
+
800
+ if err := client.LogoutAuthSession(context.Background(), &RefreshRequest{RefreshToken: "refresh-logout"}); err != nil {
801
+ t.Fatalf("LogoutAuthSession returned error: %v", err)
802
+ }
803
+ }
804
+
805
+ func TestIssueStreamTicketPostsBearerBodyAndDecodesData(t *testing.T) {
806
+ expiresAt := time.Date(2026, 6, 9, 12, 0, 0, 0, time.UTC)
807
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
808
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/auth/stream-ticket")
809
+ body := decodeJSONRequestBody[StreamTicketRequest](t, r)
810
+ if got := body.Path; got != "/api/v1/events/stream" {
811
+ t.Fatalf("stream ticket path = %q, want /api/v1/events/stream", got)
812
+ }
813
+ writeJSON(t, w, map[string]any{
814
+ "data": map[string]any{
815
+ "ticket": "ticket-1",
816
+ "expiresAt": expiresAt,
817
+ },
818
+ })
819
+ })
820
+
821
+ ticket, err := client.IssueStreamTicket(context.Background(), StreamTicketRequest{Path: "/api/v1/events/stream"})
822
+ if err != nil {
823
+ t.Fatalf("IssueStreamTicket returned error: %v", err)
824
+ }
825
+ if got := ticket.Ticket; got != "ticket-1" {
826
+ t.Fatalf("stream ticket = %q, want ticket-1", got)
827
+ }
828
+ if got := ticket.ExpiresAt; !got.Equal(expiresAt) {
829
+ t.Fatalf("stream ticket ExpiresAt = %s, want %s", got, expiresAt)
830
+ }
831
+ }
832
+
833
+ func TestListAIGatewayAuditLogsBuildsRequestAndDecodesItems(t *testing.T) {
834
+ from := time.Date(2026, 6, 10, 8, 9, 10, 123000000, time.UTC)
835
+ to := time.Date(2026, 6, 10, 9, 9, 10, 0, time.UTC)
836
+ createdAt := time.Date(2026, 6, 10, 8, 30, 0, 0, time.UTC)
837
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
838
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/audit-logs")
839
+ assertQuery(t, r, map[string]string{
840
+ "action": "tool.invoke",
841
+ "actorId": "user 1",
842
+ "actorType": "user",
843
+ "aiClientId": "client-1",
844
+ "approvalRequestId": "approval-1",
845
+ "from": from.Format(time.RFC3339Nano),
846
+ "limit": "50",
847
+ "result": "success",
848
+ "riskLevel": "high",
849
+ "skillId": "skill-1",
850
+ "to": to.Format(time.RFC3339Nano),
851
+ "toolName": "deploy.apply",
852
+ })
853
+ writeJSON(t, w, map[string]any{
854
+ "items": []map[string]any{
855
+ {
856
+ "id": "audit-1",
857
+ "action": "tool.invoke",
858
+ "actorId": "user-1",
859
+ "actorType": "user",
860
+ "createdAt": createdAt,
861
+ "result": "success",
862
+ "riskLevel": "high",
863
+ "summary": "tool invocation allowed",
864
+ "toolName": "deploy.apply",
865
+ "aiClientId": "client-1",
866
+ "aiClientName": "CLI",
867
+ },
868
+ },
869
+ })
870
+ })
871
+
872
+ items, err := client.ListAIGatewayAuditLogs(context.Background(), ListAIGatewayAuditLogsParams{
873
+ Action: "tool.invoke",
874
+ ActorID: "user 1",
875
+ ActorType: "user",
876
+ AIClientID: "client-1",
877
+ ApprovalRequestID: "approval-1",
878
+ From: from,
879
+ Limit: 50,
880
+ Result: "success",
881
+ RiskLevel: "high",
882
+ SkillID: "skill-1",
883
+ To: to,
884
+ ToolName: "deploy.apply",
885
+ })
886
+ if err != nil {
887
+ t.Fatalf("ListAIGatewayAuditLogs returned error: %v", err)
888
+ }
889
+ if len(items) != 1 {
890
+ t.Fatalf("got %d audit logs, want 1", len(items))
891
+ }
892
+ if got := items[0].ID; got != "audit-1" {
893
+ t.Fatalf("audit log ID = %q, want audit-1", got)
894
+ }
895
+ if got := items[0].CreatedAt; !got.Equal(createdAt) {
896
+ t.Fatalf("audit log CreatedAt = %s, want %s", got, createdAt)
897
+ }
898
+ }
899
+
900
+ func TestListAIGatewayApprovalRequestsBuildsRequestAndDecodesItems(t *testing.T) {
901
+ from := time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
902
+ to := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)
903
+ createdAt := time.Date(2026, 6, 1, 10, 0, 0, 0, time.UTC)
904
+ updatedAt := time.Date(2026, 6, 1, 10, 5, 0, 0, time.UTC)
905
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
906
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/approval-requests")
907
+ assertQuery(t, r, map[string]string{
908
+ "actorId": "user-1",
909
+ "actorType": "user",
910
+ "aiClientId": "client-1",
911
+ "from": from.Format(time.RFC3339Nano),
912
+ "id": "approval-1",
913
+ "limit": "25",
914
+ "riskLevel": "critical",
915
+ "skillId": "skill-1",
916
+ "status": "pending",
917
+ "strategy": "multi-stage",
918
+ "to": to.Format(time.RFC3339Nano),
919
+ "toolName": "deploy.apply",
920
+ })
921
+ writeJSON(t, w, map[string]any{
922
+ "items": []map[string]any{
923
+ {
924
+ "id": "approval-1",
925
+ "actorId": "user-1",
926
+ "actorType": "user",
927
+ "createdAt": createdAt,
928
+ "requiresApproval": true,
929
+ "riskLevel": "critical",
930
+ "status": "pending",
931
+ "strategy": "multi-stage",
932
+ "summary": "deployment requires approval",
933
+ "toolName": "deploy.apply",
934
+ "updatedAt": updatedAt,
935
+ },
936
+ },
937
+ })
938
+ })
939
+
940
+ items, err := client.ListAIGatewayApprovalRequests(context.Background(), ListAIGatewayApprovalRequestsParams{
941
+ ActorID: "user-1",
942
+ ActorType: "user",
943
+ AIClientID: "client-1",
944
+ From: from,
945
+ ID: "approval-1",
946
+ Limit: 25,
947
+ RiskLevel: "critical",
948
+ SkillID: "skill-1",
949
+ Status: "pending",
950
+ Strategy: "multi-stage",
951
+ To: to,
952
+ ToolName: "deploy.apply",
953
+ })
954
+ if err != nil {
955
+ t.Fatalf("ListAIGatewayApprovalRequests returned error: %v", err)
956
+ }
957
+ if len(items) != 1 {
958
+ t.Fatalf("got %d approval requests, want 1", len(items))
959
+ }
960
+ if got := items[0].Status; got != "pending" {
961
+ t.Fatalf("approval status = %q, want pending", got)
962
+ }
963
+ if got := items[0].RiskLevel; got != "critical" {
964
+ t.Fatalf("approval risk level = %q, want critical", got)
965
+ }
966
+ }
967
+
968
+ func TestGetAIGatewayApprovalTimelineEscapesPathAndDecodesData(t *testing.T) {
969
+ createdAt := time.Date(2026, 6, 3, 12, 0, 0, 0, time.UTC)
970
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
971
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/approval-requests/request%2F1/timeline")
972
+ writeJSON(t, w, map[string]any{
973
+ "data": map[string]any{
974
+ "request": map[string]any{
975
+ "id": "request/1",
976
+ "actorId": "user-1",
977
+ "actorType": "user",
978
+ "createdAt": createdAt,
979
+ "requiresApproval": true,
980
+ "riskLevel": "high",
981
+ "status": "pending",
982
+ "strategy": "manual",
983
+ "summary": "manual approval",
984
+ "toolName": "deploy.apply",
985
+ "updatedAt": createdAt,
986
+ },
987
+ "events": []map[string]any{
988
+ {
989
+ "id": "event-1",
990
+ "action": "created",
991
+ "createdAt": createdAt,
992
+ "kind": "approval",
993
+ "result": "pending",
994
+ },
995
+ },
996
+ },
997
+ })
998
+ })
999
+
1000
+ timeline, err := client.GetAIGatewayApprovalTimeline(context.Background(), "request/1")
1001
+ if err != nil {
1002
+ t.Fatalf("GetAIGatewayApprovalTimeline returned error: %v", err)
1003
+ }
1004
+ if got := timeline.Request.ID; got != "request/1" {
1005
+ t.Fatalf("timeline request ID = %q, want request/1", got)
1006
+ }
1007
+ if len(timeline.Events) != 1 {
1008
+ t.Fatalf("got %d timeline events, want 1", len(timeline.Events))
1009
+ }
1010
+ }
1011
+
1012
+ func TestDecideAIGatewayApprovalRequestPostsBodyAndDecodesData(t *testing.T) {
1013
+ decidedAt := time.Date(2026, 6, 4, 13, 0, 0, 0, time.UTC)
1014
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1015
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/approval-requests/request%2F1/approve")
1016
+ if got := r.Header.Get("Content-Type"); got != "application/json" {
1017
+ t.Fatalf("Content-Type = %q, want application/json", got)
1018
+ }
1019
+ var body ApprovalDecisionInput
1020
+ raw, err := io.ReadAll(r.Body)
1021
+ if err != nil {
1022
+ t.Fatalf("read request body: %v", err)
1023
+ }
1024
+ if err := json.Unmarshal(raw, &body); err != nil {
1025
+ t.Fatalf("decode request body %q: %v", string(raw), err)
1026
+ }
1027
+ if got := body.Comment; got != "ship it" {
1028
+ t.Fatalf("decision comment = %q, want ship it", got)
1029
+ }
1030
+ writeJSON(t, w, map[string]any{
1031
+ "data": map[string]any{
1032
+ "request": map[string]any{
1033
+ "id": "request/1",
1034
+ "actorId": "user-1",
1035
+ "actorType": "user",
1036
+ "createdAt": decidedAt,
1037
+ "decidedAt": decidedAt,
1038
+ "decisionComment": "ship it",
1039
+ "requiresApproval": true,
1040
+ "riskLevel": "high",
1041
+ "status": "approved",
1042
+ "strategy": "manual",
1043
+ "summary": "manual approval",
1044
+ "toolName": "deploy.apply",
1045
+ "updatedAt": decidedAt,
1046
+ },
1047
+ "invocation": map[string]any{
1048
+ "requiresApproval": false,
1049
+ "result": "success",
1050
+ "riskLevel": "high",
1051
+ "toolName": "deploy.apply",
1052
+ },
1053
+ },
1054
+ })
1055
+ })
1056
+
1057
+ result, err := client.DecideAIGatewayApprovalRequest(context.Background(), "request/1", Approve, &ApprovalDecisionInput{Comment: "ship it"})
1058
+ if err != nil {
1059
+ t.Fatalf("DecideAIGatewayApprovalRequest returned error: %v", err)
1060
+ }
1061
+ if got := result.Request.Status; got != "approved" {
1062
+ t.Fatalf("decision request status = %q, want approved", got)
1063
+ }
1064
+ if result.Invocation == nil {
1065
+ t.Fatal("decision invocation is nil, want decoded invocation")
1066
+ }
1067
+ if got := result.Invocation.Result; got != "success" {
1068
+ t.Fatalf("decision invocation result = %q, want success", got)
1069
+ }
1070
+ }
1071
+
1072
+ func TestGetAIGatewayGovernanceStatusBuildsRequestAndDecodesData(t *testing.T) {
1073
+ generatedAt := time.Date(2026, 6, 5, 14, 0, 0, 0, time.UTC)
1074
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1075
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/governance/status")
1076
+ assertQuery(t, r, map[string]string{"windowHours": "24"})
1077
+ writeJSON(t, w, map[string]any{
1078
+ "data": map[string]any{
1079
+ "generatedAt": generatedAt,
1080
+ "windowHours": 24,
1081
+ "health": map[string]any{"status": "healthy", "message": "all checks passed"},
1082
+ "approvals": map[string]any{"pending": 2, "overdue": 1, "stalePending": 1, "dueSoon": 1},
1083
+ "clients": map[string]any{"total": 3, "active": 2, "disabled": 1, "pendingApproval": 0, "registrationApproval": "manual"},
1084
+ "metrics": map[string]any{"totalCalls": 10, "successCount": 8, "failureCount": 1, "denyCount": 1, "dryRunCount": 0, "pendingApprovalCount": 2},
1085
+ "redaction": map[string]any{"totalMatches": 1, "auditsWithRedaction": 1, "fieldMatches": 1, "inputAudits": 1, "outputAudits": 0, "secretClassifierMatches": 0, "sensitiveKeyMatches": 1, "sensitiveTextMatches": 0, "structuredSecretMatches": 0, "valuePatternMatches": 0},
1086
+ "policyCoverage": map[string]any{
1087
+ "accessPolicies": 1, "activeAccessPolicies": 1, "activeSkillBindings": 1, "activeToolGrants": 1, "budgetPolicies": 1,
1088
+ "budgetState": "enabled", "rateLimitPolicies": 1, "rateLimitState": "enabled", "redactionPolicies": 1, "redactionPolicyState": "enabled",
1089
+ "resourceScopeState": "enabled", "resourceScopedAccessPolicies": 1, "resourceScopedToolGrants": 1, "skillBindings": 1, "toolGrants": 1,
1090
+ },
1091
+ "tokens": map[string]any{
1092
+ "lastUsedTrackingState": "enabled",
1093
+ "personalAccessTokens": map[string]any{"total": 2, "active": 1, "revoked": 1, "expired": 0, "expiringSoon": 0, "neverUsed": 1, "stale": 0},
1094
+ "serviceAccountTokens": map[string]any{"total": 1, "active": 1, "revoked": 0, "expired": 0, "expiringSoon": 0, "neverUsed": 0, "stale": 0},
1095
+ },
1096
+ },
1097
+ })
1098
+ })
1099
+
1100
+ status, err := client.GetAIGatewayGovernanceStatus(context.Background(), GetAIGatewayGovernanceStatusParams{WindowHours: 24})
1101
+ if err != nil {
1102
+ t.Fatalf("GetAIGatewayGovernanceStatus returned error: %v", err)
1103
+ }
1104
+ if got := status.WindowHours; got != 24 {
1105
+ t.Fatalf("window hours = %d, want 24", got)
1106
+ }
1107
+ if got := status.Health.Status; got != "healthy" {
1108
+ t.Fatalf("health status = %q, want healthy", got)
1109
+ }
1110
+ if got := status.Tokens.PersonalAccessTokens.Total; got != 2 {
1111
+ t.Fatalf("personal access token count = %d, want 2", got)
1112
+ }
1113
+ }
1114
+
1115
+ func TestListPersonalAccessTokensBuildsRequestAndDecodesItems(t *testing.T) {
1116
+ createdAt := time.Date(2026, 6, 6, 8, 0, 0, 0, time.UTC)
1117
+ updatedAt := time.Date(2026, 6, 6, 8, 1, 0, 0, time.UTC)
1118
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1119
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/personal-access-tokens")
1120
+ assertQuery(t, r, map[string]string{
1121
+ "scope": "ai:gateway",
1122
+ "userId": "user-1",
1123
+ })
1124
+ writeJSON(t, w, map[string]any{
1125
+ "items": []map[string]any{
1126
+ {
1127
+ "id": "pat-1",
1128
+ "userId": "user-1",
1129
+ "name": "CLI",
1130
+ "tokenPrefix": "soha_pat_1234",
1131
+ "scopes": []string{"ai:gateway"},
1132
+ "permissionKeys": []string{"ai_gateway.audit.read"},
1133
+ "createdBy": "user-1",
1134
+ "createdAt": createdAt,
1135
+ "updatedAt": updatedAt,
1136
+ },
1137
+ },
1138
+ })
1139
+ })
1140
+
1141
+ items, err := client.ListPersonalAccessTokens(context.Background(), ListPersonalAccessTokensParams{
1142
+ Scope: "ai:gateway",
1143
+ UserID: "user-1",
1144
+ })
1145
+ if err != nil {
1146
+ t.Fatalf("ListPersonalAccessTokens returned error: %v", err)
1147
+ }
1148
+ if len(items) != 1 {
1149
+ t.Fatalf("got %d personal access tokens, want 1", len(items))
1150
+ }
1151
+ if got := items[0].ID; got != "pat-1" {
1152
+ t.Fatalf("personal access token ID = %q, want pat-1", got)
1153
+ }
1154
+ if got := items[0].UpdatedAt; !got.Equal(updatedAt) {
1155
+ t.Fatalf("personal access token UpdatedAt = %s, want %s", got, updatedAt)
1156
+ }
1157
+ }
1158
+
1159
+ func TestCreatePersonalAccessTokenPostsBodyAndDecodesData(t *testing.T) {
1160
+ expiresAt := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
1161
+ createdAt := time.Date(2026, 6, 6, 9, 0, 0, 0, time.UTC)
1162
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1163
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/personal-access-tokens")
1164
+ body := decodeJSONRequestBody[PersonalAccessTokenInput](t, r)
1165
+ if got := body.Name; got != "release bot" {
1166
+ t.Fatalf("personal access token name = %q, want release bot", got)
1167
+ }
1168
+ if body.ExpiresAt == nil || !body.ExpiresAt.Equal(expiresAt) {
1169
+ t.Fatalf("personal access token ExpiresAt = %v, want %s", body.ExpiresAt, expiresAt)
1170
+ }
1171
+ if got := body.Metadata["purpose"]; got != "release" {
1172
+ t.Fatalf("personal access token metadata purpose = %v, want release", got)
1173
+ }
1174
+ writeJSON(t, w, map[string]any{
1175
+ "data": map[string]any{
1176
+ "value": "soha_pat_secret",
1177
+ "token": map[string]any{
1178
+ "id": "pat-2",
1179
+ "userId": "user-1",
1180
+ "name": "release bot",
1181
+ "tokenPrefix": "soha_pat_5678",
1182
+ "scopes": []string{"ai:gateway"},
1183
+ "permissionKeys": []string{"ai_gateway.tokens.write"},
1184
+ "expiresAt": expiresAt,
1185
+ "createdBy": "user-1",
1186
+ "createdAt": createdAt,
1187
+ "updatedAt": createdAt,
1188
+ },
1189
+ },
1190
+ })
1191
+ })
1192
+
1193
+ result, err := client.CreatePersonalAccessToken(context.Background(), PersonalAccessTokenInput{
1194
+ Name: "release bot",
1195
+ Scopes: []string{"ai:gateway"},
1196
+ PermissionKeys: []string{"ai_gateway.tokens.write"},
1197
+ Metadata: map[string]any{"purpose": "release"},
1198
+ ExpiresAt: &expiresAt,
1199
+ })
1200
+ if err != nil {
1201
+ t.Fatalf("CreatePersonalAccessToken returned error: %v", err)
1202
+ }
1203
+ if got := result.Value; got != "soha_pat_secret" {
1204
+ t.Fatalf("created personal access token value = %q, want soha_pat_secret", got)
1205
+ }
1206
+ if got := result.Token.ID; got != "pat-2" {
1207
+ t.Fatalf("created personal access token ID = %q, want pat-2", got)
1208
+ }
1209
+ if result.Token.ExpiresAt == nil || !result.Token.ExpiresAt.Equal(expiresAt) {
1210
+ t.Fatalf("created personal access token ExpiresAt = %v, want %s", result.Token.ExpiresAt, expiresAt)
1211
+ }
1212
+ }
1213
+
1214
+ func TestRevokePersonalAccessTokenEscapesPathAndDecodesStatus(t *testing.T) {
1215
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1216
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/personal-access-tokens/pat%2F1/revoke")
1217
+ writeJSON(t, w, map[string]any{"status": "revoked"})
1218
+ })
1219
+
1220
+ status, err := client.RevokePersonalAccessToken(context.Background(), "pat/1")
1221
+ if err != nil {
1222
+ t.Fatalf("RevokePersonalAccessToken returned error: %v", err)
1223
+ }
1224
+ if got := status.Status; got != "revoked" {
1225
+ t.Fatalf("personal access token revoke status = %q, want revoked", got)
1226
+ }
1227
+ }
1228
+
1229
+ func TestRotatePersonalAccessTokenEscapesPathPostsBodyAndDecodesData(t *testing.T) {
1230
+ expiresAt := time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC)
1231
+ rotatedAt := time.Date(2026, 6, 6, 10, 0, 0, 0, time.UTC)
1232
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1233
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/personal-access-tokens/pat%2F1/rotate")
1234
+ body := decodeJSONRequestBody[TokenRotationInput](t, r)
1235
+ if body.ExpiresAt == nil || !body.ExpiresAt.Equal(expiresAt) {
1236
+ t.Fatalf("personal access token rotation ExpiresAt = %v, want %s", body.ExpiresAt, expiresAt)
1237
+ }
1238
+ writeJSON(t, w, map[string]any{
1239
+ "data": map[string]any{
1240
+ "value": "soha_pat_rotated",
1241
+ "token": map[string]any{
1242
+ "id": "pat-rotated",
1243
+ "userId": "user-1",
1244
+ "name": "rotated",
1245
+ "tokenPrefix": "soha_pat_9999",
1246
+ "scopes": []string{"ai:gateway"},
1247
+ "permissionKeys": []string{"ai_gateway.tokens.write"},
1248
+ "expiresAt": expiresAt,
1249
+ "createdBy": "user-1",
1250
+ "createdAt": rotatedAt,
1251
+ "updatedAt": rotatedAt,
1252
+ },
1253
+ },
1254
+ })
1255
+ })
1256
+
1257
+ result, err := client.RotatePersonalAccessToken(context.Background(), "pat/1", &TokenRotationInput{ExpiresAt: &expiresAt})
1258
+ if err != nil {
1259
+ t.Fatalf("RotatePersonalAccessToken returned error: %v", err)
1260
+ }
1261
+ if got := result.Value; got != "soha_pat_rotated" {
1262
+ t.Fatalf("rotated personal access token value = %q, want soha_pat_rotated", got)
1263
+ }
1264
+ if got := result.Token.ID; got != "pat-rotated" {
1265
+ t.Fatalf("rotated personal access token ID = %q, want pat-rotated", got)
1266
+ }
1267
+ }
1268
+
1269
+ func TestListServiceAccountsBuildsRequestAndDecodesItems(t *testing.T) {
1270
+ createdAt := time.Date(2026, 6, 7, 8, 0, 0, 0, time.UTC)
1271
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1272
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/service-accounts")
1273
+ assertQuery(t, r, map[string]string{})
1274
+ writeJSON(t, w, map[string]any{
1275
+ "items": []map[string]any{
1276
+ {
1277
+ "id": "svc-1",
1278
+ "name": "deploy service",
1279
+ "description": "automation",
1280
+ "status": "active",
1281
+ "roleIds": []string{"runner"},
1282
+ "teamIds": []string{"platform"},
1283
+ "scopeGrantIds": []string{"gateway"},
1284
+ "createdBy": "user-1",
1285
+ "createdAt": createdAt,
1286
+ "updatedAt": createdAt,
1287
+ },
1288
+ },
1289
+ })
1290
+ })
1291
+
1292
+ items, err := client.ListServiceAccounts(context.Background())
1293
+ if err != nil {
1294
+ t.Fatalf("ListServiceAccounts returned error: %v", err)
1295
+ }
1296
+ if len(items) != 1 {
1297
+ t.Fatalf("got %d service accounts, want 1", len(items))
1298
+ }
1299
+ if got := items[0].ID; got != "svc-1" {
1300
+ t.Fatalf("service account ID = %q, want svc-1", got)
1301
+ }
1302
+ if got := items[0].RoleIDs[0]; got != "runner" {
1303
+ t.Fatalf("service account role ID = %q, want runner", got)
1304
+ }
1305
+ }
1306
+
1307
+ func TestCreateServiceAccountPostsBodyAndDecodesData(t *testing.T) {
1308
+ createdAt := time.Date(2026, 6, 7, 9, 0, 0, 0, time.UTC)
1309
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1310
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/service-accounts")
1311
+ body := decodeJSONRequestBody[ServiceAccountInput](t, r)
1312
+ if got := body.ID; got != "svc-create" {
1313
+ t.Fatalf("service account input ID = %q, want svc-create", got)
1314
+ }
1315
+ if got := body.Metadata["owner"]; got != "platform" {
1316
+ t.Fatalf("service account metadata owner = %v, want platform", got)
1317
+ }
1318
+ writeJSON(t, w, map[string]any{
1319
+ "data": map[string]any{
1320
+ "id": "svc-create",
1321
+ "name": "created service",
1322
+ "description": "created by test",
1323
+ "status": "active",
1324
+ "ownerUserId": "user-1",
1325
+ "roleIds": []string{"runner"},
1326
+ "teamIds": []string{"platform"},
1327
+ "scopeGrantIds": []string{"gateway"},
1328
+ "metadata": map[string]any{"owner": "platform"},
1329
+ "createdBy": "user-1",
1330
+ "createdAt": createdAt,
1331
+ "updatedAt": createdAt,
1332
+ },
1333
+ })
1334
+ })
1335
+
1336
+ result, err := client.CreateServiceAccount(context.Background(), ServiceAccountInput{
1337
+ ID: "svc-create",
1338
+ Name: "created service",
1339
+ Description: "created by test",
1340
+ Status: "active",
1341
+ OwnerUserID: "user-1",
1342
+ RoleIDs: []string{"runner"},
1343
+ TeamIDs: []string{"platform"},
1344
+ ScopeGrantIDs: []string{"gateway"},
1345
+ Metadata: map[string]any{"owner": "platform"},
1346
+ })
1347
+ if err != nil {
1348
+ t.Fatalf("CreateServiceAccount returned error: %v", err)
1349
+ }
1350
+ if got := result.ID; got != "svc-create" {
1351
+ t.Fatalf("created service account ID = %q, want svc-create", got)
1352
+ }
1353
+ if got := result.OwnerUserID; got != "user-1" {
1354
+ t.Fatalf("created service account owner = %q, want user-1", got)
1355
+ }
1356
+ }
1357
+
1358
+ func TestListServiceAccountTokensBuildsRequestAndDecodesItems(t *testing.T) {
1359
+ createdAt := time.Date(2026, 6, 7, 10, 0, 0, 0, time.UTC)
1360
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1361
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/ai-gateway/service-account-tokens")
1362
+ assertQuery(t, r, map[string]string{})
1363
+ writeJSON(t, w, map[string]any{
1364
+ "items": []map[string]any{
1365
+ {
1366
+ "id": "sat-1",
1367
+ "serviceAccountId": "svc-1",
1368
+ "name": "deploy token",
1369
+ "tokenPrefix": "soha_sat_1234",
1370
+ "scopes": []string{"ai:gateway"},
1371
+ "permissionKeys": []string{"ai_gateway.tools.invoke"},
1372
+ "createdBy": "user-1",
1373
+ "createdAt": createdAt,
1374
+ "updatedAt": createdAt,
1375
+ },
1376
+ },
1377
+ })
1378
+ })
1379
+
1380
+ items, err := client.ListServiceAccountTokens(context.Background())
1381
+ if err != nil {
1382
+ t.Fatalf("ListServiceAccountTokens returned error: %v", err)
1383
+ }
1384
+ if len(items) != 1 {
1385
+ t.Fatalf("got %d service account tokens, want 1", len(items))
1386
+ }
1387
+ if got := items[0].ServiceAccountID; got != "svc-1" {
1388
+ t.Fatalf("service account token service account ID = %q, want svc-1", got)
1389
+ }
1390
+ }
1391
+
1392
+ func TestCreateServiceAccountTokenEscapesPathPostsBodyAndDecodesData(t *testing.T) {
1393
+ expiresAt := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
1394
+ createdAt := time.Date(2026, 6, 7, 11, 0, 0, 0, time.UTC)
1395
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1396
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/service-accounts/svc%2F1/tokens")
1397
+ body := decodeJSONRequestBody[ServiceAccountTokenInput](t, r)
1398
+ if got := body.Name; got != "deploy token" {
1399
+ t.Fatalf("service account token name = %q, want deploy token", got)
1400
+ }
1401
+ if body.ExpiresAt == nil || !body.ExpiresAt.Equal(expiresAt) {
1402
+ t.Fatalf("service account token ExpiresAt = %v, want %s", body.ExpiresAt, expiresAt)
1403
+ }
1404
+ writeJSON(t, w, map[string]any{
1405
+ "data": map[string]any{
1406
+ "value": "soha_sat_secret",
1407
+ "token": map[string]any{
1408
+ "id": "sat-2",
1409
+ "serviceAccountId": "svc/1",
1410
+ "name": "deploy token",
1411
+ "tokenPrefix": "soha_sat_5678",
1412
+ "scopes": []string{"ai:gateway"},
1413
+ "permissionKeys": []string{"ai_gateway.tools.invoke"},
1414
+ "expiresAt": expiresAt,
1415
+ "createdBy": "user-1",
1416
+ "createdAt": createdAt,
1417
+ "updatedAt": createdAt,
1418
+ },
1419
+ },
1420
+ })
1421
+ })
1422
+
1423
+ result, err := client.CreateServiceAccountToken(context.Background(), "svc/1", ServiceAccountTokenInput{
1424
+ Name: "deploy token",
1425
+ Scopes: []string{"ai:gateway"},
1426
+ PermissionKeys: []string{"ai_gateway.tools.invoke"},
1427
+ ExpiresAt: &expiresAt,
1428
+ })
1429
+ if err != nil {
1430
+ t.Fatalf("CreateServiceAccountToken returned error: %v", err)
1431
+ }
1432
+ if got := result.Value; got != "soha_sat_secret" {
1433
+ t.Fatalf("created service account token value = %q, want soha_sat_secret", got)
1434
+ }
1435
+ if got := result.Token.ServiceAccountID; got != "svc/1" {
1436
+ t.Fatalf("created service account token service account ID = %q, want svc/1", got)
1437
+ }
1438
+ }
1439
+
1440
+ func TestRevokeServiceAccountTokenEscapesPathAndDecodesStatus(t *testing.T) {
1441
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1442
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/service-account-tokens/sat%2F1/revoke")
1443
+ writeJSON(t, w, map[string]any{"status": "revoked"})
1444
+ })
1445
+
1446
+ status, err := client.RevokeServiceAccountToken(context.Background(), "sat/1")
1447
+ if err != nil {
1448
+ t.Fatalf("RevokeServiceAccountToken returned error: %v", err)
1449
+ }
1450
+ if got := status.Status; got != "revoked" {
1451
+ t.Fatalf("service account token revoke status = %q, want revoked", got)
1452
+ }
1453
+ }
1454
+
1455
+ func TestRotateServiceAccountTokenEscapesPathPostsBodyAndDecodesData(t *testing.T) {
1456
+ expiresAt := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)
1457
+ rotatedAt := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
1458
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1459
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/ai-gateway/service-account-tokens/sat%2F1/rotate")
1460
+ body := decodeJSONRequestBody[TokenRotationInput](t, r)
1461
+ if body.ExpiresAt == nil || !body.ExpiresAt.Equal(expiresAt) {
1462
+ t.Fatalf("service account token rotation ExpiresAt = %v, want %s", body.ExpiresAt, expiresAt)
1463
+ }
1464
+ writeJSON(t, w, map[string]any{
1465
+ "data": map[string]any{
1466
+ "value": "soha_sat_rotated",
1467
+ "token": map[string]any{
1468
+ "id": "sat-rotated",
1469
+ "serviceAccountId": "svc-1",
1470
+ "name": "rotated service token",
1471
+ "tokenPrefix": "soha_sat_9999",
1472
+ "scopes": []string{"ai:gateway"},
1473
+ "permissionKeys": []string{"ai_gateway.tools.invoke"},
1474
+ "expiresAt": expiresAt,
1475
+ "createdBy": "user-1",
1476
+ "createdAt": rotatedAt,
1477
+ "updatedAt": rotatedAt,
1478
+ },
1479
+ },
1480
+ })
1481
+ })
1482
+
1483
+ result, err := client.RotateServiceAccountToken(context.Background(), "sat/1", &TokenRotationInput{ExpiresAt: &expiresAt})
1484
+ if err != nil {
1485
+ t.Fatalf("RotateServiceAccountToken returned error: %v", err)
1486
+ }
1487
+ if got := result.Value; got != "soha_sat_rotated" {
1488
+ t.Fatalf("rotated service account token value = %q, want soha_sat_rotated", got)
1489
+ }
1490
+ if got := result.Token.ID; got != "sat-rotated" {
1491
+ t.Fatalf("rotated service account token ID = %q, want sat-rotated", got)
1492
+ }
1493
+ }
1494
+
1495
+ func TestListMCPCapabilitiesBuildsRequestAndDecodesItems(t *testing.T) {
1496
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1497
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/mcp/capabilities")
1498
+ assertQuery(t, r, map[string]string{})
1499
+ writeJSON(t, w, map[string]any{
1500
+ "items": []map[string]any{
1501
+ {
1502
+ "adapterID": "filesystem",
1503
+ "name": "resource.read",
1504
+ "description": "read files through the gateway",
1505
+ "scopes": []string{"resources:read"},
1506
+ },
1507
+ },
1508
+ })
1509
+ })
1510
+
1511
+ items, err := client.ListMCPCapabilities(context.Background())
1512
+ if err != nil {
1513
+ t.Fatalf("ListMCPCapabilities returned error: %v", err)
1514
+ }
1515
+ if len(items) != 1 {
1516
+ t.Fatalf("got %d MCP capabilities, want 1", len(items))
1517
+ }
1518
+ if got := items[0].AdapterID; got != "filesystem" {
1519
+ t.Fatalf("MCP capability adapter ID = %q, want filesystem", got)
1520
+ }
1521
+ if got := items[0].Scopes[0]; got != "resources:read" {
1522
+ t.Fatalf("MCP capability scope = %q, want resources:read", got)
1523
+ }
1524
+ }
1525
+
1526
+ func TestListMarketplacePluginsBuildsRequestAndDecodesItems(t *testing.T) {
1527
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1528
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/plugins/marketplace")
1529
+ assertQuery(t, r, map[string]string{
1530
+ "publisher": "OpenSoha",
1531
+ "q": "k8s sre",
1532
+ "type": "skill-pack",
1533
+ })
1534
+ writeJSON(t, w, map[string]any{
1535
+ "items": []map[string]any{
1536
+ marketplacePluginJSON("k8s-sre", true),
1537
+ },
1538
+ })
1539
+ })
1540
+
1541
+ items, err := client.ListMarketplacePlugins(context.Background(), ListMarketplacePluginsParams{
1542
+ Publisher: "OpenSoha",
1543
+ Q: "k8s sre",
1544
+ Type: "skill-pack",
1545
+ })
1546
+ if err != nil {
1547
+ t.Fatalf("ListMarketplacePlugins returned error: %v", err)
1548
+ }
1549
+ if len(items) != 1 {
1550
+ t.Fatalf("got %d marketplace plugins, want 1", len(items))
1551
+ }
1552
+ if got := items[0].ID; got != "k8s-sre" {
1553
+ t.Fatalf("marketplace plugin ID = %q, want k8s-sre", got)
1554
+ }
1555
+ if got := items[0].Manifest.Capabilities.Tools[0]; got != "k8s.rollout" {
1556
+ t.Fatalf("marketplace plugin tool = %q, want k8s.rollout", got)
1557
+ }
1558
+ if !items[0].Installed {
1559
+ t.Fatal("marketplace plugin Installed = false, want true")
1560
+ }
1561
+ }
1562
+
1563
+ func TestGetMarketplacePluginEscapesPathAndDecodesData(t *testing.T) {
1564
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1565
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/plugins/marketplace/plugin%2F1")
1566
+ writeJSON(t, w, map[string]any{
1567
+ "data": marketplacePluginJSON("plugin/1", false),
1568
+ })
1569
+ })
1570
+
1571
+ plugin, err := client.GetMarketplacePlugin(context.Background(), "plugin/1")
1572
+ if err != nil {
1573
+ t.Fatalf("GetMarketplacePlugin returned error: %v", err)
1574
+ }
1575
+ if got := plugin.ID; got != "plugin/1" {
1576
+ t.Fatalf("marketplace plugin ID = %q, want plugin/1", got)
1577
+ }
1578
+ if got := plugin.Source; got != "marketplace" {
1579
+ t.Fatalf("marketplace plugin source = %q, want marketplace", got)
1580
+ }
1581
+ }
1582
+
1583
+ func TestListInstalledPluginsBuildsRequestAndDecodesItems(t *testing.T) {
1584
+ installedAt := time.Date(2026, 6, 8, 9, 0, 0, 0, time.UTC)
1585
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1586
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/plugins/installed")
1587
+ assertQuery(t, r, map[string]string{})
1588
+ writeJSON(t, w, map[string]any{
1589
+ "items": []map[string]any{
1590
+ installedPluginJSON("k8s-sre", "enabled", installedAt),
1591
+ },
1592
+ })
1593
+ })
1594
+
1595
+ items, err := client.ListInstalledPlugins(context.Background())
1596
+ if err != nil {
1597
+ t.Fatalf("ListInstalledPlugins returned error: %v", err)
1598
+ }
1599
+ if len(items) != 1 {
1600
+ t.Fatalf("got %d installed plugins, want 1", len(items))
1601
+ }
1602
+ if got := items[0].Status; got != "enabled" {
1603
+ t.Fatalf("installed plugin status = %q, want enabled", got)
1604
+ }
1605
+ if got := items[0].InstalledAt; !got.Equal(installedAt) {
1606
+ t.Fatalf("installed plugin InstalledAt = %s, want %s", got, installedAt)
1607
+ }
1608
+ }
1609
+
1610
+ func TestInstallPluginPostsBodyAndDecodesData(t *testing.T) {
1611
+ installedAt := time.Date(2026, 6, 8, 10, 0, 0, 0, time.UTC)
1612
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1613
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/plugins/install")
1614
+ body := decodeJSONRequestBody[PluginInstallRequest](t, r)
1615
+ if got := body.PluginID; got != "k8s-sre" {
1616
+ t.Fatalf("plugin install PluginID = %q, want k8s-sre", got)
1617
+ }
1618
+ if got := body.Source; got != "marketplace" {
1619
+ t.Fatalf("plugin install Source = %q, want marketplace", got)
1620
+ }
1621
+ if got := body.ExpectedChecksum; got != "sha256:abc123" {
1622
+ t.Fatalf("plugin install ExpectedChecksum = %q, want sha256:abc123", got)
1623
+ }
1624
+ if !body.Enable {
1625
+ t.Fatal("plugin install Enable = false, want true")
1626
+ }
1627
+ if body.Manifest == nil || body.Manifest.ID != "custom-plugin" {
1628
+ t.Fatalf("plugin install Manifest ID = %v, want custom-plugin", body.Manifest)
1629
+ }
1630
+ writeJSON(t, w, map[string]any{
1631
+ "data": installedPluginJSON("k8s-sre", "enabled", installedAt),
1632
+ })
1633
+ })
1634
+
1635
+ plugin, err := client.InstallPlugin(context.Background(), PluginInstallRequest{
1636
+ Enable: true,
1637
+ ExpectedChecksum: "sha256:abc123",
1638
+ Manifest: &PluginManifest{
1639
+ ID: "custom-plugin",
1640
+ Name: "Custom Plugin",
1641
+ Publisher: "OpenSoha",
1642
+ Type: "skill-pack",
1643
+ Version: "1.0.0",
1644
+ },
1645
+ PluginID: "k8s-sre",
1646
+ Source: "marketplace",
1647
+ })
1648
+ if err != nil {
1649
+ t.Fatalf("InstallPlugin returned error: %v", err)
1650
+ }
1651
+ if got := plugin.ID; got != "k8s-sre" {
1652
+ t.Fatalf("installed plugin ID = %q, want k8s-sre", got)
1653
+ }
1654
+ if got := plugin.Manifest.ID; got != "k8s-sre" {
1655
+ t.Fatalf("installed plugin manifest ID = %q, want k8s-sre", got)
1656
+ }
1657
+ }
1658
+
1659
+ func TestGetInstalledPluginEscapesPathAndDecodesData(t *testing.T) {
1660
+ installedAt := time.Date(2026, 6, 8, 11, 0, 0, 0, time.UTC)
1661
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1662
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/plugins/plugin%2F1")
1663
+ writeJSON(t, w, map[string]any{
1664
+ "data": installedPluginJSON("plugin/1", "enabled", installedAt),
1665
+ })
1666
+ })
1667
+
1668
+ plugin, err := client.GetInstalledPlugin(context.Background(), "plugin/1")
1669
+ if err != nil {
1670
+ t.Fatalf("GetInstalledPlugin returned error: %v", err)
1671
+ }
1672
+ if got := plugin.ID; got != "plugin/1" {
1673
+ t.Fatalf("installed plugin ID = %q, want plugin/1", got)
1674
+ }
1675
+ if got := plugin.Type; got != "skill-pack" {
1676
+ t.Fatalf("installed plugin type = %q, want skill-pack", got)
1677
+ }
1678
+ }
1679
+
1680
+ func TestRemoveInstalledPluginEscapesPathAndDecodesStatus(t *testing.T) {
1681
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1682
+ assertCommonRequest(t, r, http.MethodDelete, "/api/v1/plugins/plugin%2F1")
1683
+ writeJSON(t, w, map[string]any{"status": "removed"})
1684
+ })
1685
+
1686
+ status, err := client.RemoveInstalledPlugin(context.Background(), "plugin/1")
1687
+ if err != nil {
1688
+ t.Fatalf("RemoveInstalledPlugin returned error: %v", err)
1689
+ }
1690
+ if got := status.Status; got != "removed" {
1691
+ t.Fatalf("remove installed plugin status = %q, want removed", got)
1692
+ }
1693
+ }
1694
+
1695
+ func TestGetInstalledPluginManifestEscapesPathAndDecodesData(t *testing.T) {
1696
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1697
+ assertCommonRequest(t, r, http.MethodGet, "/api/v1/plugins/plugin%2F1/manifest")
1698
+ writeJSON(t, w, map[string]any{
1699
+ "data": pluginManifestJSON("plugin/1"),
1700
+ })
1701
+ })
1702
+
1703
+ manifest, err := client.GetInstalledPluginManifest(context.Background(), "plugin/1")
1704
+ if err != nil {
1705
+ t.Fatalf("GetInstalledPluginManifest returned error: %v", err)
1706
+ }
1707
+ if got := manifest.ID; got != "plugin/1" {
1708
+ t.Fatalf("installed plugin manifest ID = %q, want plugin/1", got)
1709
+ }
1710
+ if got := manifest.Permissions.Required[0]; got != "mcp.capabilities.read" {
1711
+ t.Fatalf("installed plugin manifest permission = %q, want mcp.capabilities.read", got)
1712
+ }
1713
+ }
1714
+
1715
+ func TestInstalledPluginStateMethodsEscapePathAndDecodeData(t *testing.T) {
1716
+ cases := []struct {
1717
+ name string
1718
+ path string
1719
+ status string
1720
+ call func(*Client) (InstalledPlugin, error)
1721
+ }{
1722
+ {
1723
+ name: "enable",
1724
+ path: "/api/v1/plugins/plugin%2F1/enable",
1725
+ status: "enabled",
1726
+ call: func(client *Client) (InstalledPlugin, error) {
1727
+ return client.EnableInstalledPlugin(context.Background(), "plugin/1")
1728
+ },
1729
+ },
1730
+ {
1731
+ name: "disable",
1732
+ path: "/api/v1/plugins/plugin%2F1/disable",
1733
+ status: "disabled",
1734
+ call: func(client *Client) (InstalledPlugin, error) {
1735
+ return client.DisableInstalledPlugin(context.Background(), "plugin/1")
1736
+ },
1737
+ },
1738
+ }
1739
+ for _, tt := range cases {
1740
+ t.Run(tt.name, func(t *testing.T) {
1741
+ updatedAt := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
1742
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1743
+ assertCommonRequest(t, r, http.MethodPost, tt.path)
1744
+ writeJSON(t, w, map[string]any{
1745
+ "data": installedPluginJSON("plugin/1", tt.status, updatedAt),
1746
+ })
1747
+ })
1748
+
1749
+ plugin, err := tt.call(client)
1750
+ if err != nil {
1751
+ t.Fatalf("%s installed plugin returned error: %v", tt.name, err)
1752
+ }
1753
+ if got := plugin.Status; got != tt.status {
1754
+ t.Fatalf("%s installed plugin status = %q, want %s", tt.name, got, tt.status)
1755
+ }
1756
+ })
1757
+ }
1758
+ }
1759
+
1760
+ func TestUpgradeInstalledPluginEscapesPathPostsBodyAndDecodesData(t *testing.T) {
1761
+ updatedAt := time.Date(2026, 6, 8, 13, 0, 0, 0, time.UTC)
1762
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1763
+ assertCommonRequest(t, r, http.MethodPost, "/api/v1/plugins/plugin%2F1/upgrade")
1764
+ body := decodeJSONRequestBody[PluginInstallRequest](t, r)
1765
+ if got := body.Source; got != "marketplace" {
1766
+ t.Fatalf("plugin upgrade Source = %q, want marketplace", got)
1767
+ }
1768
+ if got := body.ExpectedChecksum; got != "sha256:def456" {
1769
+ t.Fatalf("plugin upgrade ExpectedChecksum = %q, want sha256:def456", got)
1770
+ }
1771
+ writeJSON(t, w, map[string]any{
1772
+ "data": installedPluginJSON("plugin/1", "enabled", updatedAt),
1773
+ })
1774
+ })
1775
+
1776
+ plugin, err := client.UpgradeInstalledPlugin(context.Background(), "plugin/1", &PluginInstallRequest{
1777
+ ExpectedChecksum: "sha256:def456",
1778
+ Source: "marketplace",
1779
+ })
1780
+ if err != nil {
1781
+ t.Fatalf("UpgradeInstalledPlugin returned error: %v", err)
1782
+ }
1783
+ if got := plugin.ID; got != "plugin/1" {
1784
+ t.Fatalf("upgraded installed plugin ID = %q, want plugin/1", got)
1785
+ }
1786
+ }
1787
+
1788
+ func TestConfigureInstalledPluginEscapesPathPostsBodyAndDecodesData(t *testing.T) {
1789
+ enabled := false
1790
+ updatedAt := time.Date(2026, 6, 8, 14, 0, 0, 0, time.UTC)
1791
+ client := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
1792
+ assertCommonRequest(t, r, http.MethodPut, "/api/v1/plugins/plugin%2F1/config")
1793
+ body := decodeJSONRequestBody[PluginConfigRequest](t, r)
1794
+ if body.Enabled == nil || *body.Enabled {
1795
+ t.Fatalf("plugin config Enabled = %v, want false", body.Enabled)
1796
+ }
1797
+ if got := body.Metadata["env"]; got != "prod" {
1798
+ t.Fatalf("plugin config metadata env = %v, want prod", got)
1799
+ }
1800
+ if got := body.SecretRefs["apiToken"]; got != "secret/ref" {
1801
+ t.Fatalf("plugin config secret ref = %q, want secret/ref", got)
1802
+ }
1803
+ writeJSON(t, w, map[string]any{
1804
+ "data": installedPluginJSON("plugin/1", "disabled", updatedAt),
1805
+ })
1806
+ })
1807
+
1808
+ plugin, err := client.ConfigureInstalledPlugin(context.Background(), "plugin/1", PluginConfigRequest{
1809
+ Enabled: &enabled,
1810
+ Metadata: map[string]any{"env": "prod"},
1811
+ SecretRefs: map[string]string{"apiToken": "secret/ref"},
1812
+ })
1813
+ if err != nil {
1814
+ t.Fatalf("ConfigureInstalledPlugin returned error: %v", err)
1815
+ }
1816
+ if got := plugin.Status; got != "disabled" {
1817
+ t.Fatalf("configured installed plugin status = %q, want disabled", got)
1818
+ }
1819
+ }
1820
+
1821
+ func TestAgentRunCallbackRequestEventsWireShape(t *testing.T) {
1822
+ legacy := AgentRunCallbackRequest{
1823
+ AgentID: "agent-1",
1824
+ CallbackToken: "agent-run-token",
1825
+ Payload: map[string]any{"summary": "completed"},
1826
+ RunID: "run-1",
1827
+ Status: "succeeded",
1828
+ }
1829
+ raw, err := json.Marshal(legacy)
1830
+ if err != nil {
1831
+ t.Fatalf("marshal legacy callback: %v", err)
1832
+ }
1833
+ var legacyWire map[string]any
1834
+ if err := json.Unmarshal(raw, &legacyWire); err != nil {
1835
+ t.Fatalf("decode legacy callback JSON: %v", err)
1836
+ }
1837
+ if _, ok := legacyWire["events"]; ok {
1838
+ t.Fatal("legacy callback emitted events field, want omitted")
1839
+ }
1840
+
1841
+ var thinkingDelta AgentRunCallbackWorkbenchStreamEvent
1842
+ if err := thinkingDelta.FromAgentRunCallbackWorkbenchThinkingDeltaEvent(AgentRunCallbackWorkbenchThinkingDeltaEvent{
1843
+ TextDelta: "Checking recent deployment events.",
1844
+ Type: "thinking.delta",
1845
+ }); err != nil {
1846
+ t.Fatalf("build callback thinking.delta event: %v", err)
1847
+ }
1848
+ var toolDelta AgentRunCallbackWorkbenchStreamEvent
1849
+ if err := toolDelta.FromAgentRunCallbackWorkbenchToolDeltaEvent(AgentRunCallbackWorkbenchToolDeltaEvent{
1850
+ LogDelta: "kubectl get events returned 12 rows",
1851
+ ToolCallID: "tool-1",
1852
+ Type: "tool.delta",
1853
+ }); err != nil {
1854
+ t.Fatalf("build callback tool.delta event: %v", err)
1855
+ }
1856
+ var toolCompleted AgentRunCallbackWorkbenchStreamEvent
1857
+ if err := toolCompleted.FromAgentRunCallbackWorkbenchToolCompletedEvent(AgentRunCallbackWorkbenchToolCompletedEvent{
1858
+ ToolCall: WorkbenchToolCall{
1859
+ AdapterID: "kubernetes",
1860
+ ID: "tool-1",
1861
+ Status: "success",
1862
+ Summary: "Collected recent warning events",
1863
+ ToolName: "events.list",
1864
+ },
1865
+ Type: "tool.completed",
1866
+ }); err != nil {
1867
+ t.Fatalf("build callback tool.completed event: %v", err)
1868
+ }
1869
+ var messageDone AgentRunCallbackWorkbenchStreamEvent
1870
+ if err := messageDone.FromAgentRunCallbackWorkbenchMessageDoneEvent(AgentRunCallbackWorkbenchMessageDoneEvent{
1871
+ Content: "The rollout failed because the new pod image could not be pulled.",
1872
+ Role: "assistant",
1873
+ Type: "message.done",
1874
+ }); err != nil {
1875
+ t.Fatalf("build callback message.done event: %v", err)
1876
+ }
1877
+
1878
+ withEvents := legacy
1879
+ withEvents.Events = []AgentRunCallbackWorkbenchStreamEvent{
1880
+ thinkingDelta,
1881
+ toolDelta,
1882
+ toolCompleted,
1883
+ messageDone,
1884
+ }
1885
+ raw, err = json.Marshal(withEvents)
1886
+ if err != nil {
1887
+ t.Fatalf("marshal callback with events: %v", err)
1888
+ }
1889
+ var eventWire map[string]any
1890
+ if err := json.Unmarshal(raw, &eventWire); err != nil {
1891
+ t.Fatalf("decode callback with events JSON: %v", err)
1892
+ }
1893
+ events, ok := eventWire["events"].([]any)
1894
+ if !ok || len(events) != 4 {
1895
+ t.Fatalf("events wire value = %#v, want four events", eventWire["events"])
1896
+ }
1897
+ wantTypes := []string{"thinking.delta", "tool.delta", "tool.completed", "message.done"}
1898
+ serverOwnedFields := []string{"id", "sessionId", "runId", "sequence", "createdAt"}
1899
+ for i, rawEvent := range events {
1900
+ event, ok := rawEvent.(map[string]any)
1901
+ if !ok {
1902
+ t.Fatalf("events[%d] = %#v, want object", i, rawEvent)
1903
+ }
1904
+ if got := event["type"]; got != wantTypes[i] {
1905
+ t.Fatalf("events[%d] type = %v, want %s", i, got, wantTypes[i])
1906
+ }
1907
+ for _, field := range serverOwnedFields {
1908
+ if _, ok := event[field]; ok {
1909
+ t.Fatalf("events[%d] emitted server-owned field %q in callback input", i, field)
1910
+ }
1911
+ }
1912
+ }
1913
+
1914
+ var roundTrip AgentRunCallbackRequest
1915
+ if err := json.Unmarshal(raw, &roundTrip); err != nil {
1916
+ t.Fatalf("unmarshal callback with events: %v", err)
1917
+ }
1918
+ if len(roundTrip.Events) != 4 {
1919
+ t.Fatalf("round-trip events length = %d, want 4", len(roundTrip.Events))
1920
+ }
1921
+ thinking, err := roundTrip.Events[0].AsAgentRunCallbackWorkbenchThinkingDeltaEvent()
1922
+ if err != nil {
1923
+ t.Fatalf("decode round-trip thinking.delta event: %v", err)
1924
+ }
1925
+ if got := thinking.TextDelta; got != "Checking recent deployment events." {
1926
+ t.Fatalf("round-trip thinking.delta text = %q", got)
1927
+ }
1928
+ delta, err := roundTrip.Events[1].AsAgentRunCallbackWorkbenchToolDeltaEvent()
1929
+ if err != nil {
1930
+ t.Fatalf("decode round-trip tool.delta event: %v", err)
1931
+ }
1932
+ if got := delta.LogDelta; got != "kubectl get events returned 12 rows" {
1933
+ t.Fatalf("round-trip tool.delta log = %q", got)
1934
+ }
1935
+ completed, err := roundTrip.Events[2].AsAgentRunCallbackWorkbenchToolCompletedEvent()
1936
+ if err != nil {
1937
+ t.Fatalf("decode round-trip tool.completed event: %v", err)
1938
+ }
1939
+ if got := completed.ToolCall.Status; got != "success" {
1940
+ t.Fatalf("round-trip tool.completed status = %q", got)
1941
+ }
1942
+ done, err := roundTrip.Events[3].AsAgentRunCallbackWorkbenchMessageDoneEvent()
1943
+ if err != nil {
1944
+ t.Fatalf("decode round-trip message.done event: %v", err)
1945
+ }
1946
+ if got := done.Content; got != "The rollout failed because the new pod image could not be pulled." {
1947
+ t.Fatalf("round-trip event content = %q", got)
1948
+ }
1949
+ if done.CreatedAt != nil || done.ID != "" || done.SessionID != "" || done.RunID != "" || done.Sequence != 0 {
1950
+ t.Fatalf("round-trip message.done carried server-owned fields: %#v", done)
1951
+ }
1952
+ }
1953
+
1954
+ func newTestClient(t *testing.T, handler http.HandlerFunc) *Client {
1955
+ t.Helper()
1956
+ server := httptest.NewServer(handler)
1957
+ t.Cleanup(server.Close)
1958
+ return NewClient(
1959
+ server.URL,
1960
+ WithBearerToken("test-token"),
1961
+ WithHTTPClient(server.Client()),
1962
+ WithUserAgent("sohaapi-test"),
1963
+ )
1964
+ }
1965
+
1966
+ func decodeJSONRequestBody[T any](t *testing.T, r *http.Request) T {
1967
+ t.Helper()
1968
+ if got := r.Header.Get("Content-Type"); got != "application/json" {
1969
+ t.Fatalf("Content-Type = %q, want application/json", got)
1970
+ }
1971
+ raw, err := io.ReadAll(r.Body)
1972
+ if err != nil {
1973
+ t.Fatalf("read request body: %v", err)
1974
+ }
1975
+ var body T
1976
+ if err := json.Unmarshal(raw, &body); err != nil {
1977
+ t.Fatalf("decode request body %q: %v", string(raw), err)
1978
+ }
1979
+ return body
1980
+ }
1981
+
1982
+ func assertCommonRequest(t *testing.T, r *http.Request, method, path string) {
1983
+ t.Helper()
1984
+ if r.Method != method {
1985
+ t.Fatalf("method = %s, want %s", r.Method, method)
1986
+ }
1987
+ if got := r.URL.EscapedPath(); got != path {
1988
+ t.Fatalf("path = %q, want %q", got, path)
1989
+ }
1990
+ if got := r.Header.Get("Accept"); got != "application/json" {
1991
+ t.Fatalf("Accept = %q, want application/json", got)
1992
+ }
1993
+ if got := r.Header.Get("Authorization"); got != "Bearer test-token" {
1994
+ t.Fatalf("Authorization = %q, want Bearer test-token", got)
1995
+ }
1996
+ if got := r.Header.Get("User-Agent"); got != "sohaapi-test" {
1997
+ t.Fatalf("User-Agent = %q, want sohaapi-test", got)
1998
+ }
1999
+ }
2000
+
2001
+ func assertPublicRequest(t *testing.T, r *http.Request, method, path string) {
2002
+ t.Helper()
2003
+ if r.Method != method {
2004
+ t.Fatalf("method = %s, want %s", r.Method, method)
2005
+ }
2006
+ if got := r.URL.EscapedPath(); got != path {
2007
+ t.Fatalf("path = %q, want %q", got, path)
2008
+ }
2009
+ if got := r.Header.Get("Accept"); got != "application/json" {
2010
+ t.Fatalf("Accept = %q, want application/json", got)
2011
+ }
2012
+ if got := r.Header.Get("Authorization"); got != "" {
2013
+ t.Fatalf("Authorization = %q, want empty", got)
2014
+ }
2015
+ if got := r.Header.Get("User-Agent"); got != "sohaapi-test" {
2016
+ t.Fatalf("User-Agent = %q, want sohaapi-test", got)
2017
+ }
2018
+ }
2019
+
2020
+ func assertQuery(t *testing.T, r *http.Request, want map[string]string) {
2021
+ t.Helper()
2022
+ got := r.URL.Query()
2023
+ if len(got) != len(want) {
2024
+ t.Fatalf("query keys = %v, want %v", got, want)
2025
+ }
2026
+ for key, value := range want {
2027
+ if gotValue := got.Get(key); gotValue != value {
2028
+ t.Fatalf("query[%s] = %q, want %q", key, gotValue, value)
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ func authResultJSON(expiresAt time.Time, accessToken string) map[string]any {
2034
+ return map[string]any{
2035
+ "data": map[string]any{
2036
+ "tokens": map[string]any{
2037
+ "accessToken": accessToken,
2038
+ "expiresAt": expiresAt,
2039
+ "expiresIn": int64(3600),
2040
+ "refreshToken": "refresh-token",
2041
+ "tokenType": "Bearer",
2042
+ },
2043
+ "user": principalJSON("user-1"),
2044
+ },
2045
+ }
2046
+ }
2047
+
2048
+ func principalJSON(userID string) map[string]any {
2049
+ return map[string]any{
2050
+ "userId": userID,
2051
+ "userName": "admin",
2052
+ "email": "admin@example.com",
2053
+ "roles": []string{"admin"},
2054
+ "teams": []string{"platform"},
2055
+ "projects": []string{"default"},
2056
+ "tags": []string{"ops"},
2057
+ "permissionKeys": []string{"auth.read"},
2058
+ }
2059
+ }
2060
+
2061
+ func writeJSON(t *testing.T, w http.ResponseWriter, value any) {
2062
+ t.Helper()
2063
+ w.Header().Set("Content-Type", "application/json")
2064
+ if err := json.NewEncoder(w).Encode(value); err != nil {
2065
+ t.Fatalf("write response JSON: %v", err)
2066
+ }
2067
+ }
2068
+
2069
+ func marketplacePluginJSON(id string, installed bool) map[string]any {
2070
+ return map[string]any{
2071
+ "id": id,
2072
+ "name": "Kubernetes SRE",
2073
+ "publisher": "OpenSoha",
2074
+ "riskLevel": "high",
2075
+ "source": "marketplace",
2076
+ "summary": "Kubernetes operations through OpenSoha",
2077
+ "type": "skill-pack",
2078
+ "version": "1.0.0",
2079
+ "installed": installed,
2080
+ "manifest": pluginManifestJSON(id),
2081
+ }
2082
+ }
2083
+
2084
+ func installedPluginJSON(id, status string, timestamp time.Time) map[string]any {
2085
+ value := map[string]any{
2086
+ "id": id,
2087
+ "name": "Kubernetes SRE",
2088
+ "publisher": "OpenSoha",
2089
+ "type": "skill-pack",
2090
+ "version": "1.0.0",
2091
+ "status": status,
2092
+ "source": "marketplace",
2093
+ "manifest": pluginManifestJSON(id),
2094
+ "checksumStatus": "verified",
2095
+ "signatureStatus": "verified",
2096
+ "requestedPermissions": map[string]any{"required": []string{"mcp.capabilities.read"}},
2097
+ "configuredSecretRefs": map[string]string{"apiToken": "secret/ref"},
2098
+ "metadata": map[string]any{"env": "prod"},
2099
+ "installedBy": "user-1",
2100
+ "installedAt": timestamp,
2101
+ "updatedAt": timestamp,
2102
+ }
2103
+ if status == "enabled" {
2104
+ value["enabledAt"] = timestamp
2105
+ }
2106
+ if status == "disabled" {
2107
+ value["disabledAt"] = timestamp
2108
+ }
2109
+ return value
2110
+ }
2111
+
2112
+ func pluginManifestJSON(id string) map[string]any {
2113
+ return map[string]any{
2114
+ "id": id,
2115
+ "name": "Kubernetes SRE",
2116
+ "publisher": "OpenSoha",
2117
+ "type": "skill-pack",
2118
+ "version": "1.0.0",
2119
+ "description": "Kubernetes operations through OpenSoha",
2120
+ "capabilities": map[string]any{
2121
+ "tools": []string{"k8s.rollout"},
2122
+ },
2123
+ "permissions": map[string]any{
2124
+ "required": []string{"mcp.capabilities.read"},
2125
+ },
2126
+ }
2127
+ }