@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

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 (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,689 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ createInferenceServiceController,
5
+ validateKradleInferenceService,
6
+ createResource,
7
+ KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY,
8
+ KSERVE_API_GROUP,
9
+ KSERVE_API_VERSION,
10
+ SUPPORTED_MODEL_FORMATS,
11
+ INFERENCE_PROTOCOLS
12
+ } from '../src/index.js';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // KServe Inference Service Controller Tests
16
+ //
17
+ // A KradleInferenceService wraps KServe InferenceService resources for model
18
+ // inference with endpoint discovery, V1/V2 protocol support, provider config
19
+ // bridge, health checks, and model metadata.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function makeInferenceService(name, overrides = {}) {
23
+ return createResource('KradleInferenceService', { name, namespace: 'kradle-org-default' }, {
24
+ organizationRef: 'default',
25
+ modelFormat: 'sklearn',
26
+ storageUri: 's3://models/sklearn/iris',
27
+ ...overrides
28
+ });
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // 1. Factory and shape
33
+ // ---------------------------------------------------------------------------
34
+
35
+ test('createInferenceServiceController returns controller with expected methods', () => {
36
+ const controller = createInferenceServiceController();
37
+ assert.ok(controller, 'controller must be truthy');
38
+ assert.equal(typeof controller.validate, 'function', 'controller must expose validate');
39
+ assert.equal(typeof controller.generateKServeManifest, 'function', 'controller must expose generateKServeManifest');
40
+ assert.equal(typeof controller.resolveEndpoint, 'function', 'controller must expose resolveEndpoint');
41
+ assert.equal(typeof controller.toProviderConfig, 'function', 'controller must expose toProviderConfig');
42
+ assert.equal(typeof controller.infer, 'function', 'controller must expose infer');
43
+ assert.equal(typeof controller.checkHealth, 'function', 'controller must expose checkHealth');
44
+ assert.equal(typeof controller.getModelMetadata, 'function', 'controller must expose getModelMetadata');
45
+ assert.equal(typeof controller.listModels, 'function', 'controller must expose listModels');
46
+ assert.equal(controller.role, 'kradle-inference-service-controller', 'controller must declare its role');
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // 2. validate — accepts valid sklearn InferenceService
51
+ // ---------------------------------------------------------------------------
52
+
53
+ test('validate accepts valid sklearn InferenceService', () => {
54
+ const controller = createInferenceServiceController();
55
+ const resource = makeInferenceService('sklearn-iris');
56
+ const result = controller.validate(resource);
57
+
58
+ assert.equal(result.valid, true, 'valid sklearn resource must pass validation');
59
+ assert.equal(result.errors.length, 0, 'errors array must be empty');
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // 3. validate — accepts valid pytorch with GPU resources
64
+ // ---------------------------------------------------------------------------
65
+
66
+ test('validate accepts valid pytorch with GPU resources', () => {
67
+ const controller = createInferenceServiceController();
68
+ const resource = makeInferenceService('pytorch-resnet', {
69
+ modelFormat: 'pytorch',
70
+ storageUri: 's3://models/pytorch/resnet50',
71
+ resources: { limits: { cpu: '4', memory: '16Gi', 'nvidia.com/gpu': '1' } }
72
+ });
73
+ const result = controller.validate(resource);
74
+
75
+ assert.equal(result.valid, true, 'pytorch resource with GPU must pass validation');
76
+ assert.equal(result.errors.length, 0, 'errors array must be empty');
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // 4. validate — rejects unknown model format
81
+ // ---------------------------------------------------------------------------
82
+
83
+ test('validate rejects unknown model format', () => {
84
+ const controller = createInferenceServiceController();
85
+ const resource = makeInferenceService('bad-format', { modelFormat: 'caffe' });
86
+ const result = controller.validate(resource);
87
+
88
+ assert.equal(result.valid, false, 'unknown model format must fail validation');
89
+ assert.ok(result.errors.length > 0, 'errors must not be empty');
90
+ assert.ok(result.errors.some(e => /modelFormat/i.test(e)), 'error must mention modelFormat');
91
+ assert.ok(result.errors.some(e => SUPPORTED_MODEL_FORMATS.some(f => e.includes(f))), 'error must enumerate valid formats');
92
+ });
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // 5. validate — rejects missing storageUri
96
+ // ---------------------------------------------------------------------------
97
+
98
+ test('validate rejects missing storageUri', () => {
99
+ const controller = createInferenceServiceController();
100
+ const resource = makeInferenceService('no-storage');
101
+ delete resource.spec.storageUri;
102
+ const result = controller.validate(resource);
103
+
104
+ assert.equal(result.valid, false, 'missing storageUri must fail validation');
105
+ assert.ok(result.errors.some(e => /storageUri/i.test(e)), 'error must mention storageUri');
106
+ });
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // 6. validate — rejects invalid storageUri scheme
110
+ // ---------------------------------------------------------------------------
111
+
112
+ test('validate rejects invalid storageUri scheme', () => {
113
+ const controller = createInferenceServiceController();
114
+ const resource = makeInferenceService('bad-uri', { storageUri: 'ftp://models/bad' });
115
+ const result = controller.validate(resource);
116
+
117
+ assert.equal(result.valid, false, 'invalid storageUri scheme must fail validation');
118
+ assert.ok(result.errors.some(e => /storageUri/i.test(e)), 'error must mention storageUri');
119
+ });
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // 7. validate — rejects null resource
123
+ // ---------------------------------------------------------------------------
124
+
125
+ test('validate rejects null resource', () => {
126
+ const controller = createInferenceServiceController();
127
+ const result = controller.validate(null);
128
+
129
+ assert.equal(result.valid, false, 'null resource must fail validation');
130
+ assert.ok(result.errors.some(e => /null|undefined/i.test(e)), 'error must mention null or undefined');
131
+ });
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // 8. generateKServeManifest — produces valid KServe spec
135
+ // ---------------------------------------------------------------------------
136
+
137
+ test('generateKServeManifest produces valid KServe spec', () => {
138
+ const controller = createInferenceServiceController();
139
+ const resource = makeInferenceService('sklearn-iris', {
140
+ modelFormat: 'sklearn',
141
+ storageUri: 's3://models/sklearn/iris'
142
+ });
143
+ const manifest = controller.generateKServeManifest(resource);
144
+
145
+ assert.equal(manifest.apiVersion, 'serving.kserve.io/v1beta1', 'apiVersion must be serving.kserve.io/v1beta1');
146
+ assert.equal(manifest.kind, 'InferenceService', 'kind must be InferenceService');
147
+ assert.equal(manifest.metadata.name, 'sklearn-iris', 'name must match');
148
+ assert.equal(manifest.metadata.labels['kradle.a5c.ai/managed'], 'true', 'managed label must be set');
149
+ assert.equal(manifest.metadata.labels['kradle.a5c.ai/org'], 'default', 'org label must be set');
150
+ assert.equal(manifest.spec.predictor.modelFormat.name, 'sklearn', 'model format name must match');
151
+ assert.equal(manifest.spec.predictor.storageUri, 's3://models/sklearn/iris', 'storageUri must match');
152
+ assert.ok(manifest.spec.predictor.resources, 'resources must be present (defaults)');
153
+ });
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // 9. generateKServeManifest — includes transformer when specified
157
+ // ---------------------------------------------------------------------------
158
+
159
+ test('generateKServeManifest includes transformer when specified', () => {
160
+ const controller = createInferenceServiceController();
161
+ const resource = makeInferenceService('with-transformer', {
162
+ transformer: { containers: [{ image: 'custom-transformer:latest' }] }
163
+ });
164
+ const manifest = controller.generateKServeManifest(resource);
165
+
166
+ assert.ok(manifest.spec.transformer, 'transformer must be present in manifest');
167
+ assert.deepEqual(manifest.spec.transformer, resource.spec.transformer, 'transformer must match spec');
168
+ });
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // 10. generateKServeManifest — includes explainer when specified
172
+ // ---------------------------------------------------------------------------
173
+
174
+ test('generateKServeManifest includes explainer when specified', () => {
175
+ const controller = createInferenceServiceController();
176
+ const resource = makeInferenceService('with-explainer', {
177
+ explainer: { containers: [{ image: 'alibi-explainer:latest' }] }
178
+ });
179
+ const manifest = controller.generateKServeManifest(resource);
180
+
181
+ assert.ok(manifest.spec.explainer, 'explainer must be present in manifest');
182
+ assert.deepEqual(manifest.spec.explainer, resource.spec.explainer, 'explainer must match spec');
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // 11. generateKServeManifest — omits transformer and explainer when not specified
187
+ // ---------------------------------------------------------------------------
188
+
189
+ test('generateKServeManifest omits transformer and explainer when not specified', () => {
190
+ const controller = createInferenceServiceController();
191
+ const resource = makeInferenceService('plain-predictor');
192
+ const manifest = controller.generateKServeManifest(resource);
193
+
194
+ assert.equal(manifest.spec.transformer, undefined, 'transformer must not be present');
195
+ assert.equal(manifest.spec.explainer, undefined, 'explainer must not be present');
196
+ });
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // 12. resolveEndpoint — extracts URL from status
200
+ // ---------------------------------------------------------------------------
201
+
202
+ test('resolveEndpoint extracts URL from status', () => {
203
+ const controller = createInferenceServiceController();
204
+ const isvc = {
205
+ metadata: { name: 'my-model', namespace: 'inference' },
206
+ status: { url: 'https://my-model.inference.example.com' }
207
+ };
208
+ const endpoint = controller.resolveEndpoint(isvc);
209
+
210
+ assert.equal(endpoint, 'https://my-model.inference.example.com', 'must return status URL');
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // 13. resolveEndpoint — extracts URL from status.address.url
215
+ // ---------------------------------------------------------------------------
216
+
217
+ test('resolveEndpoint extracts URL from status.address.url', () => {
218
+ const controller = createInferenceServiceController();
219
+ const isvc = {
220
+ metadata: { name: 'my-model', namespace: 'inference' },
221
+ status: { address: { url: 'http://my-model.inference.svc.cluster.local' } }
222
+ };
223
+ const endpoint = controller.resolveEndpoint(isvc);
224
+
225
+ assert.equal(endpoint, 'http://my-model.inference.svc.cluster.local', 'must return address URL');
226
+ });
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // 14. resolveEndpoint — generates cluster-internal URL as fallback
230
+ // ---------------------------------------------------------------------------
231
+
232
+ test('resolveEndpoint generates cluster-internal URL as fallback', () => {
233
+ const controller = createInferenceServiceController();
234
+ const isvc = {
235
+ metadata: { name: 'my-model', namespace: 'inference' },
236
+ status: {}
237
+ };
238
+ const endpoint = controller.resolveEndpoint(isvc);
239
+
240
+ assert.equal(endpoint, 'http://my-model.inference.svc.cluster.local', 'must generate cluster-internal URL');
241
+ });
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // 15. toProviderConfig — creates valid provider with kserve type
245
+ // ---------------------------------------------------------------------------
246
+
247
+ test('toProviderConfig creates valid provider config with kserve type', () => {
248
+ const controller = createInferenceServiceController();
249
+ const resource = makeInferenceService('sklearn-iris');
250
+ const config = controller.toProviderConfig(resource, 'http://sklearn-iris.default.svc.cluster.local');
251
+
252
+ assert.equal(config.providerType, 'kserve', 'providerType must be kserve');
253
+ assert.equal(config.endpoint, 'http://sklearn-iris.default.svc.cluster.local', 'endpoint must match');
254
+ assert.ok(config.modelCatalog, 'modelCatalog must be present');
255
+ assert.ok(Array.isArray(config.modelCatalog), 'modelCatalog must be an array');
256
+ assert.ok(config.rateLimits, 'rateLimits must be present');
257
+ });
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // 16. toProviderConfig — includes v2 protocol by default
261
+ // ---------------------------------------------------------------------------
262
+
263
+ test('toProviderConfig includes v2 protocol by default', () => {
264
+ const controller = createInferenceServiceController();
265
+ const resource = makeInferenceService('default-protocol');
266
+ const config = controller.toProviderConfig(resource, 'http://localhost:8080');
267
+
268
+ assert.equal(config.protocol, 'v2', 'default protocol must be v2');
269
+ assert.equal(config.featureFlags.v2_protocol, true, 'v2_protocol flag must be true');
270
+ assert.equal(config.featureFlags.v1_protocol, false, 'v1_protocol flag must be false');
271
+ });
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // 17. toProviderConfig — includes explainability flag when explainer present
275
+ // ---------------------------------------------------------------------------
276
+
277
+ test('toProviderConfig includes explainability flag when explainer present', () => {
278
+ const controller = createInferenceServiceController();
279
+ const resource = makeInferenceService('with-explainer', {
280
+ explainer: { containers: [{ image: 'alibi:latest' }] }
281
+ });
282
+ const config = controller.toProviderConfig(resource, 'http://localhost:8080');
283
+
284
+ assert.equal(config.featureFlags.explainability, true, 'explainability flag must be true when explainer present');
285
+ });
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // 18. infer — sends V2 protocol request
289
+ // ---------------------------------------------------------------------------
290
+
291
+ test('infer sends V2 protocol request', async () => {
292
+ const controller = createInferenceServiceController();
293
+ let capturedUrl, capturedBody;
294
+ const mockFetch = async (url, opts) => {
295
+ capturedUrl = url;
296
+ capturedBody = JSON.parse(opts.body);
297
+ return { ok: true, json: async () => ({ outputs: [{ data: [1] }] }) };
298
+ };
299
+
300
+ const inputs = [{ name: 'input', shape: [1, 4], datatype: 'FP32', data: [5.1, 3.5, 1.4, 0.2] }];
301
+ const result = await controller.infer('http://localhost:8080', 'iris-model', inputs, { fetchImpl: mockFetch });
302
+
303
+ assert.ok(capturedUrl.includes('/v2/models/iris-model/infer'), 'must use V2 infer path');
304
+ assert.ok(capturedBody.inputs, 'V2 body must contain inputs array');
305
+ assert.equal(capturedBody.inputs[0].name, 'input', 'input name must match');
306
+ assert.ok(result.outputs, 'result must contain outputs');
307
+ });
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // 19. infer — sends V1 protocol request when specified
311
+ // ---------------------------------------------------------------------------
312
+
313
+ test('infer sends V1 protocol request when specified', async () => {
314
+ const controller = createInferenceServiceController();
315
+ let capturedUrl, capturedBody;
316
+ const mockFetch = async (url, opts) => {
317
+ capturedUrl = url;
318
+ capturedBody = JSON.parse(opts.body);
319
+ return { ok: true, json: async () => ({ predictions: [[1]] }) };
320
+ };
321
+
322
+ const inputs = [{ name: 'input', data: [5.1, 3.5, 1.4, 0.2] }];
323
+ const result = await controller.infer('http://localhost:8080', 'iris-model', inputs, { protocol: 'v1', fetchImpl: mockFetch });
324
+
325
+ assert.ok(capturedUrl.includes('/v1/models/iris-model:predict'), 'must use V1 predict path');
326
+ assert.ok(capturedBody.instances, 'V1 body must contain instances array');
327
+ assert.ok(result.predictions, 'result must contain predictions');
328
+ });
329
+
330
+ // ---------------------------------------------------------------------------
331
+ // 20. infer — includes auth token when provided
332
+ // ---------------------------------------------------------------------------
333
+
334
+ test('infer includes auth token when provided', async () => {
335
+ const controller = createInferenceServiceController();
336
+ let capturedHeaders;
337
+ const mockFetch = async (url, opts) => {
338
+ capturedHeaders = opts.headers;
339
+ return { ok: true, json: async () => ({ outputs: [] }) };
340
+ };
341
+
342
+ await controller.infer('http://localhost:8080', 'model', [{ name: 'x', shape: [1], datatype: 'FP32', data: [1] }], {
343
+ authToken: 'test-token-123',
344
+ fetchImpl: mockFetch
345
+ });
346
+
347
+ assert.equal(capturedHeaders.Authorization, 'Bearer test-token-123', 'must include Authorization header');
348
+ });
349
+
350
+ // ---------------------------------------------------------------------------
351
+ // 21. infer — throws on non-ok response
352
+ // ---------------------------------------------------------------------------
353
+
354
+ test('infer throws on non-ok response', async () => {
355
+ const controller = createInferenceServiceController();
356
+ const mockFetch = async () => ({
357
+ ok: false,
358
+ status: 500,
359
+ text: async () => 'Internal Server Error'
360
+ });
361
+
362
+ await assert.rejects(
363
+ () => controller.infer('http://localhost:8080', 'model', [{ name: 'x', shape: [1], datatype: 'FP32', data: [1] }], { fetchImpl: mockFetch }),
364
+ /Inference failed: 500/,
365
+ 'must throw on non-ok response'
366
+ );
367
+ });
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // 22. checkHealth — returns healthy for 200
371
+ // ---------------------------------------------------------------------------
372
+
373
+ test('checkHealth returns healthy for 200', async () => {
374
+ const controller = createInferenceServiceController();
375
+ const mockFetch = async () => ({ ok: true, status: 200 });
376
+ const health = await controller.checkHealth('http://localhost:8080', { fetchImpl: mockFetch });
377
+
378
+ assert.equal(health.healthy, true, 'must be healthy');
379
+ assert.equal(health.status, 200, 'status must be 200');
380
+ });
381
+
382
+ // ---------------------------------------------------------------------------
383
+ // 23. checkHealth — returns unhealthy for timeout
384
+ // ---------------------------------------------------------------------------
385
+
386
+ test('checkHealth returns unhealthy for timeout', async () => {
387
+ const controller = createInferenceServiceController();
388
+ const mockFetch = async () => { throw new Error('timeout exceeded'); };
389
+ const health = await controller.checkHealth('http://localhost:8080', { fetchImpl: mockFetch });
390
+
391
+ assert.equal(health.healthy, false, 'must be unhealthy');
392
+ assert.ok(health.error, 'error must be present');
393
+ assert.ok(health.error.includes('timeout'), 'error must mention timeout');
394
+ });
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // 24. getModelMetadata — returns model schema
398
+ // ---------------------------------------------------------------------------
399
+
400
+ test('getModelMetadata returns model schema', async () => {
401
+ const controller = createInferenceServiceController();
402
+ const mockMetadata = { name: 'iris', versions: ['1'], platform: 'sklearn', inputs: [], outputs: [] };
403
+ const mockFetch = async () => ({ ok: true, json: async () => mockMetadata });
404
+ const metadata = await controller.getModelMetadata('http://localhost:8080', 'iris', { fetchImpl: mockFetch });
405
+
406
+ assert.deepEqual(metadata, mockMetadata, 'must return model metadata');
407
+ });
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // 25. getModelMetadata — returns null for not found
411
+ // ---------------------------------------------------------------------------
412
+
413
+ test('getModelMetadata returns null for not found', async () => {
414
+ const controller = createInferenceServiceController();
415
+ const mockFetch = async () => ({ ok: false, status: 404 });
416
+ const metadata = await controller.getModelMetadata('http://localhost:8080', 'missing', { fetchImpl: mockFetch });
417
+
418
+ assert.equal(metadata, null, 'must return null for missing model');
419
+ });
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // 26. listModels — returns available models
423
+ // ---------------------------------------------------------------------------
424
+
425
+ test('listModels returns available models', async () => {
426
+ const controller = createInferenceServiceController();
427
+ const mockFetch = async () => ({ ok: true, json: async () => ({ models: ['iris', 'resnet'] }) });
428
+ const models = await controller.listModels('http://localhost:8080', { fetchImpl: mockFetch });
429
+
430
+ assert.deepEqual(models, ['iris', 'resnet'], 'must return model list');
431
+ });
432
+
433
+ // ---------------------------------------------------------------------------
434
+ // 27. listModels — returns empty array on error
435
+ // ---------------------------------------------------------------------------
436
+
437
+ test('listModels returns empty array on error', async () => {
438
+ const controller = createInferenceServiceController();
439
+ const mockFetch = async () => { throw new Error('connection refused'); };
440
+ const models = await controller.listModels('http://localhost:8080', { fetchImpl: mockFetch });
441
+
442
+ assert.deepEqual(models, [], 'must return empty array on error');
443
+ });
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // 28. SUPPORTED_MODEL_FORMATS includes all expected formats
447
+ // ---------------------------------------------------------------------------
448
+
449
+ test('SUPPORTED_MODEL_FORMATS includes all expected formats', () => {
450
+ const expected = ['sklearn', 'xgboost', 'lightgbm', 'tensorflow', 'pytorch', 'onnx', 'triton', 'huggingface', 'custom'];
451
+ for (const format of expected) {
452
+ assert.ok(SUPPORTED_MODEL_FORMATS.includes(format), `must include "${format}"`);
453
+ }
454
+ assert.equal(SUPPORTED_MODEL_FORMATS.length, expected.length, 'must have exactly the expected number of formats');
455
+ });
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // 29. KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY exported with correct role
459
+ // ---------------------------------------------------------------------------
460
+
461
+ test('KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY is exported with correct role', () => {
462
+ assert.ok(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
463
+ assert.equal(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY.role, 'kradle-inference-service-controller', 'role must match');
464
+ assert.ok(Array.isArray(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY.owns), 'must declare owned concerns');
465
+ });
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // 30. KSERVE_API_GROUP and KSERVE_API_VERSION exports
469
+ // ---------------------------------------------------------------------------
470
+
471
+ test('KSERVE_API_GROUP and KSERVE_API_VERSION are exported correctly', () => {
472
+ assert.equal(KSERVE_API_GROUP, 'serving.kserve.io', 'KSERVE_API_GROUP must be serving.kserve.io');
473
+ assert.equal(KSERVE_API_VERSION, 'v1beta1', 'KSERVE_API_VERSION must be v1beta1');
474
+ });
475
+
476
+ // ---------------------------------------------------------------------------
477
+ // 31. INFERENCE_PROTOCOLS exports
478
+ // ---------------------------------------------------------------------------
479
+
480
+ test('INFERENCE_PROTOCOLS exports V1 and V2', () => {
481
+ assert.equal(INFERENCE_PROTOCOLS.V1, 'v1', 'V1 must be v1');
482
+ assert.equal(INFERENCE_PROTOCOLS.V2, 'v2', 'V2 must be v2');
483
+ });
484
+
485
+ // ---------------------------------------------------------------------------
486
+ // 32. validateKradleInferenceService standalone export
487
+ // ---------------------------------------------------------------------------
488
+
489
+ test('validateKradleInferenceService standalone export works', () => {
490
+ assert.equal(typeof validateKradleInferenceService, 'function', 'must be a named export');
491
+ const resource = makeInferenceService('standalone-test');
492
+ const result = validateKradleInferenceService(resource);
493
+ assert.equal(result.valid, true, 'valid resource must pass standalone validation');
494
+ });
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // 33. validate — accepts all supported model formats
498
+ // ---------------------------------------------------------------------------
499
+
500
+ test('validate accepts all supported model formats', () => {
501
+ const controller = createInferenceServiceController();
502
+ for (const format of SUPPORTED_MODEL_FORMATS) {
503
+ const resource = makeInferenceService(`${format}-service`, { modelFormat: format });
504
+ const result = controller.validate(resource);
505
+ assert.equal(result.valid, true, `modelFormat "${format}" must pass validation`);
506
+ }
507
+ });
508
+
509
+ // ---------------------------------------------------------------------------
510
+ // 34. toProviderConfig — v1 protocol flags
511
+ // ---------------------------------------------------------------------------
512
+
513
+ test('toProviderConfig respects v1 protocol setting', () => {
514
+ const controller = createInferenceServiceController();
515
+ const resource = makeInferenceService('v1-model', { inferenceProtocol: 'v1' });
516
+ const config = controller.toProviderConfig(resource, 'http://localhost:8080');
517
+
518
+ assert.equal(config.protocol, 'v1', 'protocol must be v1');
519
+ assert.equal(config.featureFlags.v1_protocol, true, 'v1_protocol flag must be true');
520
+ assert.equal(config.featureFlags.v2_protocol, false, 'v2_protocol flag must be false');
521
+ });
522
+
523
+ // ---------------------------------------------------------------------------
524
+ // 35. generateKServeManifest — includes runtime when specified
525
+ // ---------------------------------------------------------------------------
526
+
527
+ test('generateKServeManifest includes runtime when specified', () => {
528
+ const controller = createInferenceServiceController();
529
+ const resource = makeInferenceService('with-runtime', { runtime: 'kserve-tritonserver' });
530
+ const manifest = controller.generateKServeManifest(resource);
531
+
532
+ assert.equal(manifest.spec.predictor.runtime, 'kserve-tritonserver', 'runtime must be set in predictor');
533
+ });
534
+
535
+ // ---------------------------------------------------------------------------
536
+ // 36. generateKServeManifest — omits runtime when not specified
537
+ // ---------------------------------------------------------------------------
538
+
539
+ test('generateKServeManifest omits runtime when not specified', () => {
540
+ const controller = createInferenceServiceController();
541
+ const resource = makeInferenceService('no-runtime');
542
+ const manifest = controller.generateKServeManifest(resource);
543
+
544
+ assert.equal(manifest.spec.predictor.runtime, undefined, 'runtime must not be present');
545
+ });
546
+
547
+ // ---------------------------------------------------------------------------
548
+ // 37. getSupportedModelFormats — returns copy of formats
549
+ // ---------------------------------------------------------------------------
550
+
551
+ test('getSupportedModelFormats returns supported model formats', () => {
552
+ const controller = createInferenceServiceController();
553
+ const formats = controller.getSupportedModelFormats();
554
+
555
+ assert.ok(Array.isArray(formats), 'must return an array');
556
+ assert.equal(formats.length, SUPPORTED_MODEL_FORMATS.length, 'must contain all formats');
557
+ assert.ok(formats.includes('sklearn'), 'must include sklearn');
558
+ assert.ok(formats.includes('pytorch'), 'must include pytorch');
559
+ // Verify it returns a copy, not the original
560
+ formats.push('extra');
561
+ assert.equal(SUPPORTED_MODEL_FORMATS.length, 9, 'must not modify original array');
562
+ });
563
+
564
+ // ---------------------------------------------------------------------------
565
+ // 38. validate — rejects missing organizationRef
566
+ // ---------------------------------------------------------------------------
567
+
568
+ test('validate rejects missing organizationRef', () => {
569
+ const controller = createInferenceServiceController();
570
+ const resource = {
571
+ apiVersion: 'kradle.a5c.ai/v1alpha1',
572
+ kind: 'KradleInferenceService',
573
+ metadata: { name: 'no-org', namespace: 'default', labels: {}, annotations: {} },
574
+ spec: { modelFormat: 'sklearn', storageUri: 's3://models/test' },
575
+ status: {}
576
+ };
577
+ const result = controller.validate(resource);
578
+
579
+ assert.equal(result.valid, false, 'missing organizationRef must fail');
580
+ assert.ok(result.errors.some(e => /organizationRef/i.test(e)), 'error must mention organizationRef');
581
+ });
582
+
583
+ // ---------------------------------------------------------------------------
584
+ // 39. validate — accepts pvc:// storageUri
585
+ // ---------------------------------------------------------------------------
586
+
587
+ test('validate accepts pvc:// storageUri', () => {
588
+ const controller = createInferenceServiceController();
589
+ const resource = makeInferenceService('pvc-model', { storageUri: 'pvc://model-volume/path' });
590
+ const result = controller.validate(resource);
591
+ assert.equal(result.valid, true, 'pvc:// URI must be valid');
592
+ });
593
+
594
+ // ---------------------------------------------------------------------------
595
+ // 40. validate — accepts https:// storageUri
596
+ // ---------------------------------------------------------------------------
597
+
598
+ test('validate accepts https:// storageUri', () => {
599
+ const controller = createInferenceServiceController();
600
+ const resource = makeInferenceService('https-model', { storageUri: 'https://huggingface.co/models/bert' });
601
+ const result = controller.validate(resource);
602
+ assert.equal(result.valid, true, 'https:// URI must be valid');
603
+ });
604
+
605
+ // ---------------------------------------------------------------------------
606
+ // 41. validate — accepts gs:// storageUri
607
+ // ---------------------------------------------------------------------------
608
+
609
+ test('validate accepts gs:// storageUri', () => {
610
+ const controller = createInferenceServiceController();
611
+ const resource = makeInferenceService('gcs-model', { storageUri: 'gs://bucket/models/sklearn/iris' });
612
+ const result = controller.validate(resource);
613
+ assert.equal(result.valid, true, 'gs:// URI must be valid');
614
+ });
615
+
616
+ // ---------------------------------------------------------------------------
617
+ // 42. resolveEndpoint — uses default namespace when not specified
618
+ // ---------------------------------------------------------------------------
619
+
620
+ test('resolveEndpoint uses default namespace when not specified', () => {
621
+ const controller = createInferenceServiceController();
622
+ const isvc = {
623
+ metadata: { name: 'my-model' },
624
+ status: {}
625
+ };
626
+ const endpoint = controller.resolveEndpoint(isvc);
627
+ assert.equal(endpoint, 'http://my-model.default.svc.cluster.local', 'must use default namespace');
628
+ });
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // 43. toProviderConfig — includes streaming feature flag from spec
632
+ // ---------------------------------------------------------------------------
633
+
634
+ test('toProviderConfig includes streaming feature flag from spec', () => {
635
+ const controller = createInferenceServiceController();
636
+ const resource = makeInferenceService('streaming-model', {
637
+ featureFlags: { streaming: true, batchInference: true }
638
+ });
639
+ const config = controller.toProviderConfig(resource, 'http://localhost:8080');
640
+
641
+ assert.equal(config.featureFlags.streaming, true, 'streaming must be true');
642
+ assert.equal(config.featureFlags.batch_inference, true, 'batch_inference must be true');
643
+ });
644
+
645
+ // ---------------------------------------------------------------------------
646
+ // 44. toProviderConfig — uses custom model catalog when provided
647
+ // ---------------------------------------------------------------------------
648
+
649
+ test('toProviderConfig uses custom model catalog when provided', () => {
650
+ const controller = createInferenceServiceController();
651
+ const customCatalog = [{ name: 'v1', framework: 'sklearn' }, { name: 'v2', framework: 'xgboost' }];
652
+ const resource = makeInferenceService('multi-model', { modelCatalog: customCatalog });
653
+ const config = controller.toProviderConfig(resource, 'http://localhost:8080');
654
+
655
+ assert.deepEqual(config.modelCatalog, customCatalog, 'must use custom model catalog');
656
+ });
657
+
658
+ // ---------------------------------------------------------------------------
659
+ // 45. KradleInferenceService resource creation via createResource
660
+ // ---------------------------------------------------------------------------
661
+
662
+ test('KradleInferenceService can be created via createResource', () => {
663
+ const resource = createResource('KradleInferenceService', { name: 'test-isvc', namespace: 'kradle-org-default' }, {
664
+ organizationRef: 'default',
665
+ modelFormat: 'tensorflow',
666
+ storageUri: 's3://models/tf/bert'
667
+ });
668
+
669
+ assert.equal(resource.kind, 'KradleInferenceService', 'kind must match');
670
+ assert.equal(resource.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
671
+ assert.equal(resource.spec.modelFormat, 'tensorflow', 'modelFormat must be set');
672
+ });
673
+
674
+ // ---------------------------------------------------------------------------
675
+ // 46. KradleServingRuntime resource creation via createResource
676
+ // ---------------------------------------------------------------------------
677
+
678
+ test('KradleServingRuntime can be created via createResource', () => {
679
+ const resource = createResource('KradleServingRuntime', { name: 'triton-rt', namespace: 'kradle-org-default' }, {
680
+ organizationRef: 'default',
681
+ supportedModelFormats: [{ name: 'onnx', version: '1' }],
682
+ containers: [{ name: 'triton', image: 'nvcr.io/nvidia/tritonserver:latest' }]
683
+ });
684
+
685
+ assert.equal(resource.kind, 'KradleServingRuntime', 'kind must match');
686
+ assert.equal(resource.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
687
+ assert.ok(Array.isArray(resource.spec.supportedModelFormats), 'supportedModelFormats must be array');
688
+ assert.ok(Array.isArray(resource.spec.containers), 'containers must be array');
689
+ });