@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,733 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ createModelRouteController,
5
+ createKradleApiController,
6
+ validateModelRoute,
7
+ createResource,
8
+ MODEL_ROUTE_CONTROLLER_BOUNDARY,
9
+ VALID_ROUTE_TYPES,
10
+ VALID_EXTERNAL_PROTOCOLS,
11
+ ENVOY_AI_GATEWAY_API_GROUP,
12
+ ENVOY_AI_GATEWAY_API_VERSION
13
+ } from '../src/index.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // KradleModelRoute Controller Tests
17
+ //
18
+ // A KradleModelRoute provides unified model routing through Envoy AI Gateway,
19
+ // mapping logical model names to internal KServe services or external cloud
20
+ // LLM endpoints.
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function makeInternalRoute(name, overrides = {}) {
24
+ return createResource('KradleModelRoute', { name, namespace: 'kradle-org-default' }, {
25
+ organizationRef: 'default',
26
+ modelName: `model-${name}`,
27
+ routeType: 'internal',
28
+ inferenceServiceRef: `isvc-${name}`,
29
+ ...overrides
30
+ });
31
+ }
32
+
33
+ function makeExternalRoute(name, overrides = {}) {
34
+ return createResource('KradleModelRoute', { name, namespace: 'kradle-org-default' }, {
35
+ organizationRef: 'default',
36
+ modelName: `model-${name}`,
37
+ routeType: 'external',
38
+ external: {
39
+ provider: 'openai',
40
+ endpoint: 'https://api.openai.com/v1',
41
+ protocol: 'openai',
42
+ modelId: 'gpt-4o',
43
+ authConfig: { type: 'bearer', secretRef: 'openai-api-key' }
44
+ },
45
+ ...overrides
46
+ });
47
+ }
48
+
49
+ function makeInferenceService(name, statusUrl) {
50
+ return {
51
+ kind: 'KradleInferenceService',
52
+ metadata: { name, namespace: 'kradle-org-default' },
53
+ spec: { organizationRef: 'default', modelFormat: 'pytorch', storageUri: 's3://models/test', inferenceProtocol: 'v2' },
54
+ status: statusUrl ? { url: statusUrl } : {}
55
+ };
56
+ }
57
+
58
+ function createRecordingGateway({ failKinds = new Set(), snapshotResources = [] } = {}) {
59
+ const applied = [];
60
+ return {
61
+ namespace: 'kradle-system',
62
+ resourceDefinitions: {},
63
+ applied,
64
+ async snapshot() {
65
+ return { resources: snapshotResources, namespace: 'kradle-system' };
66
+ },
67
+ async list() {
68
+ return { items: [] };
69
+ },
70
+ async get() {
71
+ return null;
72
+ },
73
+ async apply(resource) {
74
+ if (failKinds.has(resource.kind)) {
75
+ throw new Error(`apply failed for ${resource.kind}`);
76
+ }
77
+ applied.push(JSON.parse(JSON.stringify(resource)));
78
+ return { operation: 'apply', resource };
79
+ },
80
+ async delete() {
81
+ return { operation: 'delete' };
82
+ },
83
+ watch() {
84
+ return { close: () => {} };
85
+ },
86
+ };
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // 1. Factory and shape
91
+ // ---------------------------------------------------------------------------
92
+
93
+ test('createModelRouteController returns controller with expected methods', () => {
94
+ const controller = createModelRouteController();
95
+ assert.ok(controller, 'controller must be truthy');
96
+ assert.equal(typeof controller.validate, 'function', 'must expose validate');
97
+ assert.equal(typeof controller.resolveRoute, 'function', 'must expose resolveRoute');
98
+ assert.equal(typeof controller.reconcileRoutes, 'function', 'must expose reconcileRoutes');
99
+ assert.equal(typeof controller.listModelCatalog, 'function', 'must expose listModelCatalog');
100
+ assert.equal(typeof controller.generateEnvoyRouteManifest, 'function', 'must expose generateEnvoyRouteManifest');
101
+ assert.equal(typeof controller.toModelRoute, 'function', 'must expose toModelRoute');
102
+ assert.equal(controller.role, 'model-route-controller', 'must declare its role');
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // 2. validate — accepts valid internal route
107
+ // ---------------------------------------------------------------------------
108
+
109
+ test('validate accepts valid internal route', () => {
110
+ const controller = createModelRouteController();
111
+ const route = makeInternalRoute('sklearn-iris');
112
+ const result = controller.validate(route);
113
+
114
+ assert.equal(result.valid, true, 'valid internal route must pass validation');
115
+ assert.equal(result.errors.length, 0, 'errors array must be empty');
116
+ });
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // 3. validate — accepts valid external route
120
+ // ---------------------------------------------------------------------------
121
+
122
+ test('validate accepts valid external route', () => {
123
+ const controller = createModelRouteController();
124
+ const route = makeExternalRoute('gpt4');
125
+ const result = controller.validate(route);
126
+
127
+ assert.equal(result.valid, true, 'valid external route must pass validation');
128
+ assert.equal(result.errors.length, 0, 'errors array must be empty');
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // 4. validate — rejects missing modelName
133
+ // ---------------------------------------------------------------------------
134
+
135
+ test('validate rejects missing modelName', () => {
136
+ const controller = createModelRouteController();
137
+ const route = makeInternalRoute('no-model');
138
+ delete route.spec.modelName;
139
+ const result = controller.validate(route);
140
+
141
+ assert.equal(result.valid, false, 'missing modelName must fail validation');
142
+ assert.ok(result.errors.some(e => /modelName/i.test(e)), 'error must mention modelName');
143
+ });
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // 5. validate — rejects missing routeType
147
+ // ---------------------------------------------------------------------------
148
+
149
+ test('validate rejects missing routeType', () => {
150
+ const controller = createModelRouteController();
151
+ const route = makeInternalRoute('no-type');
152
+ delete route.spec.routeType;
153
+ const result = controller.validate(route);
154
+
155
+ assert.equal(result.valid, false, 'missing routeType must fail validation');
156
+ assert.ok(result.errors.some(e => /routeType/i.test(e)), 'error must mention routeType');
157
+ });
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // 6. validate — rejects invalid routeType
161
+ // ---------------------------------------------------------------------------
162
+
163
+ test('validate rejects invalid routeType (not internal/external)', () => {
164
+ const controller = createModelRouteController();
165
+ const route = makeInternalRoute('bad-type', { routeType: 'hybrid' });
166
+ const result = controller.validate(route);
167
+
168
+ assert.equal(result.valid, false, 'invalid routeType must fail validation');
169
+ assert.ok(result.errors.some(e => /routeType/i.test(e)), 'error must mention routeType');
170
+ assert.ok(result.errors.some(e => e.includes('hybrid')), 'error must include the invalid value');
171
+ });
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // 7. validate — rejects null resource
175
+ // ---------------------------------------------------------------------------
176
+
177
+ test('validate rejects null resource', () => {
178
+ const controller = createModelRouteController();
179
+ const result = controller.validate(null);
180
+
181
+ assert.equal(result.valid, false, 'null resource must fail validation');
182
+ assert.ok(result.errors.some(e => /null|undefined/i.test(e)), 'error must mention null or undefined');
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // 8. validate — rejects missing organizationRef
187
+ // ---------------------------------------------------------------------------
188
+
189
+ test('validate rejects missing organizationRef', () => {
190
+ const controller = createModelRouteController();
191
+ const route = {
192
+ apiVersion: 'kradle.a5c.ai/v1alpha1',
193
+ kind: 'KradleModelRoute',
194
+ metadata: { name: 'no-org', namespace: 'default', labels: {}, annotations: {} },
195
+ spec: { modelName: 'test-model', routeType: 'internal', inferenceServiceRef: 'isvc-test' },
196
+ status: {}
197
+ };
198
+ const result = controller.validate(route);
199
+
200
+ assert.equal(result.valid, false, 'missing organizationRef must fail');
201
+ assert.ok(result.errors.some(e => /organizationRef/i.test(e)), 'error must mention organizationRef');
202
+ });
203
+
204
+ // ---------------------------------------------------------------------------
205
+ // 9. validate — rejects internal route without inferenceServiceRef
206
+ // ---------------------------------------------------------------------------
207
+
208
+ test('validate rejects internal route without inferenceServiceRef', () => {
209
+ const controller = createModelRouteController();
210
+ const route = makeInternalRoute('no-isvc');
211
+ delete route.spec.inferenceServiceRef;
212
+ const result = controller.validate(route);
213
+
214
+ assert.equal(result.valid, false, 'internal without inferenceServiceRef must fail');
215
+ assert.ok(result.errors.some(e => /inferenceServiceRef/i.test(e)), 'error must mention inferenceServiceRef');
216
+ });
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // 10. validate — rejects external route without external block
220
+ // ---------------------------------------------------------------------------
221
+
222
+ test('validate rejects external route without external block', () => {
223
+ const controller = createModelRouteController();
224
+ const route = makeExternalRoute('no-external');
225
+ delete route.spec.external;
226
+ const result = controller.validate(route);
227
+
228
+ assert.equal(result.valid, false, 'external without external block must fail');
229
+ assert.ok(result.errors.some(e => /external/i.test(e)), 'error must mention external');
230
+ });
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // 11. validate — rejects external route with missing provider
234
+ // ---------------------------------------------------------------------------
235
+
236
+ test('validate rejects external route with missing provider', () => {
237
+ const controller = createModelRouteController();
238
+ const route = makeExternalRoute('no-provider', {
239
+ external: { endpoint: 'https://api.example.com/v1' }
240
+ });
241
+ const result = controller.validate(route);
242
+
243
+ assert.equal(result.valid, false, 'external without provider must fail');
244
+ assert.ok(result.errors.some(e => /provider/i.test(e)), 'error must mention provider');
245
+ });
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // 12. validate — rejects external route with invalid protocol
249
+ // ---------------------------------------------------------------------------
250
+
251
+ test('validate rejects external route with invalid protocol', () => {
252
+ const controller = createModelRouteController();
253
+ const route = makeExternalRoute('bad-proto', {
254
+ external: { provider: 'openai', endpoint: 'https://api.example.com/v1', protocol: 'grpc-unsupported' }
255
+ });
256
+ const result = controller.validate(route);
257
+
258
+ assert.equal(result.valid, false, 'invalid protocol must fail');
259
+ assert.ok(result.errors.some(e => /protocol/i.test(e)), 'error must mention protocol');
260
+ });
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // 13. resolveRoute — resolves internal from InferenceService status.url
264
+ // ---------------------------------------------------------------------------
265
+
266
+ test('resolveRoute resolves internal from InferenceService status.url', () => {
267
+ const controller = createModelRouteController();
268
+ const route = makeInternalRoute('iris', { inferenceServiceRef: 'sklearn-iris' });
269
+ const isvc = makeInferenceService('sklearn-iris', 'https://sklearn-iris.inference.example.com');
270
+ const resolved = controller.resolveRoute(route, [isvc]);
271
+
272
+ assert.equal(resolved.endpoint, 'https://sklearn-iris.inference.example.com', 'must resolve status URL');
273
+ assert.equal(resolved.provider, 'kserve', 'provider must be kserve');
274
+ assert.equal(resolved.protocol, 'v2', 'protocol must come from InferenceService');
275
+ });
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // 14. resolveRoute — resolves internal with cluster-internal fallback
279
+ // ---------------------------------------------------------------------------
280
+
281
+ test('resolveRoute resolves internal with cluster-internal fallback when no status URL', () => {
282
+ const controller = createModelRouteController();
283
+ const route = makeInternalRoute('fallback', { inferenceServiceRef: 'my-model' });
284
+ const isvc = makeInferenceService('my-model', null);
285
+ const resolved = controller.resolveRoute(route, [isvc]);
286
+
287
+ assert.equal(resolved.endpoint, 'http://my-model.kradle-org-default.svc.cluster.local', 'must generate cluster-internal URL');
288
+ assert.equal(resolved.provider, 'kserve', 'provider must be kserve');
289
+ });
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // 15. resolveRoute — resolves internal with no matching InferenceService
293
+ // ---------------------------------------------------------------------------
294
+
295
+ test('resolveRoute resolves internal with fallback URL when no matching InferenceService found', () => {
296
+ const controller = createModelRouteController();
297
+ const route = makeInternalRoute('orphan', { inferenceServiceRef: 'missing-isvc' });
298
+ const resolved = controller.resolveRoute(route, []);
299
+
300
+ assert.ok(resolved.endpoint, 'must still produce a fallback endpoint');
301
+ assert.ok(resolved.endpoint.includes('missing-isvc'), 'fallback must include the ref name');
302
+ assert.equal(resolved.provider, 'kserve', 'provider must be kserve');
303
+ });
304
+
305
+ // ---------------------------------------------------------------------------
306
+ // 16. resolveRoute — resolves external from spec.external fields
307
+ // ---------------------------------------------------------------------------
308
+
309
+ test('resolveRoute resolves external from spec.external fields', () => {
310
+ const controller = createModelRouteController();
311
+ const route = makeExternalRoute('gpt4');
312
+ const resolved = controller.resolveRoute(route, []);
313
+
314
+ assert.equal(resolved.endpoint, 'https://api.openai.com/v1', 'endpoint must match external.endpoint');
315
+ assert.equal(resolved.protocol, 'openai', 'protocol must match external.protocol');
316
+ assert.equal(resolved.provider, 'openai', 'provider must match external.provider');
317
+ assert.equal(resolved.modelId, 'gpt-4o', 'modelId must match external.modelId');
318
+ assert.ok(resolved.authConfig, 'authConfig must be present');
319
+ assert.equal(resolved.authConfig.type, 'bearer', 'authConfig.type must be bearer');
320
+ });
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // 17. resolveRoute — returns null endpoint for unknown routeType
324
+ // ---------------------------------------------------------------------------
325
+
326
+ test('resolveRoute returns null endpoint for unknown routeType', () => {
327
+ const controller = createModelRouteController();
328
+ const route = makeInternalRoute('unknown');
329
+ route.spec.routeType = 'unknown-type';
330
+ const resolved = controller.resolveRoute(route, []);
331
+
332
+ assert.equal(resolved.endpoint, null, 'endpoint must be null for unknown routeType');
333
+ assert.equal(resolved.provider, 'unknown', 'provider must be unknown');
334
+ });
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // 18. reconcileRoutes — marks valid routes as RouteReady True
338
+ // ---------------------------------------------------------------------------
339
+
340
+ test('reconcileRoutes marks valid routes as RouteReady True', () => {
341
+ const controller = createModelRouteController();
342
+ const route = makeExternalRoute('ready-route');
343
+ const { conditions, resolvedRoutes } = controller.reconcileRoutes([route], []);
344
+
345
+ assert.equal(conditions.length, 1, 'must have one condition');
346
+ assert.equal(conditions[0].type, 'RouteReady', 'condition type must be RouteReady');
347
+ assert.equal(conditions[0].status, 'True', 'status must be True');
348
+ assert.equal(resolvedRoutes.length, 1, 'must have one resolved route');
349
+ assert.equal(resolvedRoutes[0].modelName, route.spec.modelName, 'resolved route must have correct modelName');
350
+ });
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // 19. reconcileRoutes — marks invalid routes as RouteReady False
354
+ // ---------------------------------------------------------------------------
355
+
356
+ test('reconcileRoutes marks invalid routes as RouteReady False with ValidationFailed', () => {
357
+ const controller = createModelRouteController();
358
+ const route = makeInternalRoute('bad-route');
359
+ delete route.spec.modelName;
360
+ const { conditions, resolvedRoutes } = controller.reconcileRoutes([route], []);
361
+
362
+ assert.equal(conditions.length, 1, 'must have one condition');
363
+ assert.equal(conditions[0].status, 'False', 'status must be False');
364
+ assert.equal(conditions[0].reason, 'ValidationFailed', 'reason must be ValidationFailed');
365
+ assert.equal(resolvedRoutes.length, 0, 'must have no resolved routes');
366
+ });
367
+
368
+ // ---------------------------------------------------------------------------
369
+ // 20. listModelCatalog — merges internal and external routes
370
+ // ---------------------------------------------------------------------------
371
+
372
+ test('listModelCatalog merges both internal and external routes', () => {
373
+ const controller = createModelRouteController();
374
+ const internalRoute = makeInternalRoute('sklearn-iris', { inferenceServiceRef: 'sklearn-iris' });
375
+ const externalRoute = makeExternalRoute('gpt4');
376
+ const isvc = makeInferenceService('sklearn-iris', 'https://sklearn-iris.svc.cluster.local');
377
+ const catalog = controller.listModelCatalog([internalRoute, externalRoute], [isvc]);
378
+
379
+ assert.equal(catalog.length, 2, 'catalog must contain both routes');
380
+
381
+ const internal = catalog.find(m => m.type === 'internal');
382
+ assert.ok(internal, 'must have internal entry');
383
+ assert.equal(internal.provider, 'kserve', 'internal provider must be kserve');
384
+ assert.equal(internal.status, 'available', 'internal status must be available');
385
+ assert.equal(internal.protocol, 'v2', 'internal protocol must be v2');
386
+
387
+ const external = catalog.find(m => m.type === 'external');
388
+ assert.ok(external, 'must have external entry');
389
+ assert.equal(external.provider, 'openai', 'external provider must be openai');
390
+ assert.equal(external.status, 'available', 'external status must be available');
391
+ assert.equal(external.protocol, 'openai', 'external protocol must be openai');
392
+ });
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // 21. listModelCatalog — marks unreachable routes as unavailable
396
+ // ---------------------------------------------------------------------------
397
+
398
+ test('listModelCatalog marks invalid routes as unavailable', () => {
399
+ const controller = createModelRouteController();
400
+ const route = makeInternalRoute('broken');
401
+ delete route.spec.modelName;
402
+ const catalog = controller.listModelCatalog([route], []);
403
+
404
+ assert.equal(catalog.length, 1, 'catalog must contain the route');
405
+ assert.equal(catalog[0].status, 'unavailable', 'broken route must be unavailable');
406
+ });
407
+
408
+ // ---------------------------------------------------------------------------
409
+ // 22. generateEnvoyRouteManifest — produces valid Envoy AI Gateway config
410
+ // ---------------------------------------------------------------------------
411
+
412
+ test('generateEnvoyRouteManifest produces valid Envoy AI Gateway route config', () => {
413
+ const controller = createModelRouteController();
414
+ const route = makeExternalRoute('gpt4');
415
+ const resolvedEndpoint = { endpoint: 'https://api.openai.com/v1', protocol: 'openai', provider: 'openai', modelId: 'gpt-4o' };
416
+ const manifest = controller.generateEnvoyRouteManifest(route, resolvedEndpoint);
417
+
418
+ assert.equal(manifest.apiVersion, `${ENVOY_AI_GATEWAY_API_GROUP}/${ENVOY_AI_GATEWAY_API_VERSION}`, 'apiVersion must match');
419
+ assert.equal(manifest.kind, 'AIGatewayRoute', 'kind must be AIGatewayRoute');
420
+ assert.ok(manifest.metadata.name, 'manifest must have a name');
421
+ assert.equal(manifest.metadata.labels['kradle.a5c.ai/managed'], 'true', 'managed label must be set');
422
+ assert.equal(manifest.spec.modelName, route.spec.modelName, 'modelName must match');
423
+ assert.ok(manifest.spec.rules, 'rules must be present');
424
+ assert.ok(manifest.spec.rules.length > 0, 'must have at least one rule');
425
+ });
426
+
427
+ test('api-controller applyModelRoute applies KradleModelRoute and generated AIGatewayRoute', async () => {
428
+ const route = makeExternalRoute('gpt4');
429
+ const gateway = createRecordingGateway();
430
+ const controller = createKradleApiController({ resourceGateway: gateway });
431
+
432
+ assert.equal(typeof controller.applyModelRoute, 'function', 'controller must expose applyModelRoute');
433
+ const result = await controller.applyModelRoute(route);
434
+
435
+ const appliedKinds = gateway.applied.map((resource) => resource.kind);
436
+ assert.deepEqual(appliedKinds, ['KradleModelRoute', 'AIGatewayRoute']);
437
+ const appliedGatewayRoute = gateway.applied.find((resource) => resource.kind === 'AIGatewayRoute');
438
+ assert.equal(appliedGatewayRoute.metadata.namespace, route.metadata.namespace);
439
+ assert.equal(appliedGatewayRoute.metadata.labels['kradle.a5c.ai/model-route'], route.metadata.name);
440
+ assert.equal(result.routeResult.resource.kind, 'KradleModelRoute');
441
+ assert.equal(result.gatewayRouteResult.resource.kind, 'AIGatewayRoute');
442
+ });
443
+
444
+ test('api-controller applyModelRoute propagates generated AIGatewayRoute apply failure', async () => {
445
+ const route = makeExternalRoute('gpt4-fail');
446
+ const gateway = createRecordingGateway({ failKinds: new Set(['AIGatewayRoute']) });
447
+ const controller = createKradleApiController({ resourceGateway: gateway });
448
+
449
+ await assert.rejects(
450
+ () => controller.applyModelRoute(route),
451
+ /apply failed for AIGatewayRoute/
452
+ );
453
+ });
454
+
455
+ // ---------------------------------------------------------------------------
456
+ // 23. generateEnvoyRouteManifest — includes rate limits when specified
457
+ // ---------------------------------------------------------------------------
458
+
459
+ test('generateEnvoyRouteManifest includes rate limits when specified', () => {
460
+ const controller = createModelRouteController();
461
+ const route = makeExternalRoute('gpt4-limited', { rateLimits: { requestsPerMinute: 60 } });
462
+ const resolvedEndpoint = { endpoint: 'https://api.openai.com/v1', protocol: 'openai', provider: 'openai', modelId: 'gpt-4o' };
463
+ const manifest = controller.generateEnvoyRouteManifest(route, resolvedEndpoint);
464
+
465
+ assert.deepEqual(manifest.spec.rateLimits, { requestsPerMinute: 60 }, 'rateLimits must be included');
466
+ });
467
+
468
+ // ---------------------------------------------------------------------------
469
+ // 24. toModelRoute — converts InferenceService to KradleModelRoute spec
470
+ // ---------------------------------------------------------------------------
471
+
472
+ test('toModelRoute converts InferenceService to KradleModelRoute spec', () => {
473
+ const controller = createModelRouteController();
474
+ const isvc = {
475
+ metadata: { name: 'sklearn-iris', namespace: 'kradle-org-default', labels: { 'kradle.a5c.ai/org': 'default' } },
476
+ spec: { predictor: { modelFormat: { name: 'sklearn' }, storageUri: 's3://models/iris' } },
477
+ status: { url: 'https://sklearn-iris.inference.example.com' }
478
+ };
479
+ const resolvedEndpoint = { endpoint: 'https://sklearn-iris.inference.example.com', protocol: 'v2' };
480
+ const modelRoute = controller.toModelRoute(isvc, resolvedEndpoint);
481
+
482
+ assert.equal(modelRoute.kind, 'KradleModelRoute', 'kind must be KradleModelRoute');
483
+ assert.equal(modelRoute.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
484
+ assert.equal(modelRoute.spec.routeType, 'internal', 'routeType must be internal');
485
+ assert.equal(modelRoute.spec.inferenceServiceRef, 'sklearn-iris', 'inferenceServiceRef must match');
486
+ assert.equal(modelRoute.spec.modelName, 'sklearn-iris', 'modelName must match InferenceService name');
487
+ assert.equal(modelRoute.spec.modelFormat, 'sklearn', 'modelFormat must match predictor');
488
+ assert.equal(modelRoute.spec.endpoint, 'https://sklearn-iris.inference.example.com', 'endpoint must match resolved');
489
+ assert.equal(modelRoute.spec.protocol, 'v2', 'protocol must match');
490
+ assert.equal(modelRoute.spec.organizationRef, 'default', 'organizationRef must come from labels');
491
+ });
492
+
493
+ // ---------------------------------------------------------------------------
494
+ // 25. toModelRoute — uses organizationRef from spec when labels absent
495
+ // ---------------------------------------------------------------------------
496
+
497
+ test('toModelRoute uses spec.organizationRef when labels are absent', () => {
498
+ const controller = createModelRouteController();
499
+ const isvc = {
500
+ metadata: { name: 'bert', namespace: 'inference' },
501
+ spec: { organizationRef: 'acme', predictor: { modelFormat: { name: 'huggingface' } } },
502
+ status: {}
503
+ };
504
+ const modelRoute = controller.toModelRoute(isvc, { endpoint: 'http://bert.inference.svc.cluster.local' });
505
+
506
+ assert.equal(modelRoute.spec.organizationRef, 'acme', 'must use spec.organizationRef');
507
+ assert.equal(modelRoute.spec.modelFormat, 'huggingface', 'modelFormat must come from predictor');
508
+ });
509
+
510
+ // ---------------------------------------------------------------------------
511
+ // 26. toModelRoute — defaults to "custom" modelFormat when not available
512
+ // ---------------------------------------------------------------------------
513
+
514
+ test('toModelRoute defaults modelFormat to custom when predictor has no modelFormat', () => {
515
+ const controller = createModelRouteController();
516
+ const isvc = {
517
+ metadata: { name: 'unknown-format', namespace: 'default' },
518
+ spec: { predictor: {} },
519
+ status: {}
520
+ };
521
+ const modelRoute = controller.toModelRoute(isvc, { endpoint: 'http://unknown.default.svc.cluster.local' });
522
+
523
+ assert.equal(modelRoute.spec.modelFormat, 'custom', 'must default to custom');
524
+ });
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // 27. MODEL_ROUTE_CONTROLLER_BOUNDARY exported with correct role
528
+ // ---------------------------------------------------------------------------
529
+
530
+ test('MODEL_ROUTE_CONTROLLER_BOUNDARY is exported with correct role', () => {
531
+ assert.ok(MODEL_ROUTE_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
532
+ assert.equal(MODEL_ROUTE_CONTROLLER_BOUNDARY.role, 'model-route-controller', 'role must match');
533
+ assert.ok(Array.isArray(MODEL_ROUTE_CONTROLLER_BOUNDARY.owns), 'must declare owned concerns');
534
+ assert.ok(MODEL_ROUTE_CONTROLLER_BOUNDARY.owns.includes('route validation'), 'must own route validation');
535
+ assert.ok(MODEL_ROUTE_CONTROLLER_BOUNDARY.owns.includes('envoy route manifests'), 'must own envoy route manifests');
536
+ assert.ok(Array.isArray(MODEL_ROUTE_CONTROLLER_BOUNDARY.mustNotOwn), 'must declare mustNotOwn');
537
+ assert.ok(MODEL_ROUTE_CONTROLLER_BOUNDARY.mustNotOwn.includes('secret values'), 'must not own secret values');
538
+ });
539
+
540
+ // ---------------------------------------------------------------------------
541
+ // 28. VALID_ROUTE_TYPES and VALID_EXTERNAL_PROTOCOLS exports
542
+ // ---------------------------------------------------------------------------
543
+
544
+ test('VALID_ROUTE_TYPES and VALID_EXTERNAL_PROTOCOLS are exported correctly', () => {
545
+ assert.ok(Array.isArray(VALID_ROUTE_TYPES), 'VALID_ROUTE_TYPES must be an array');
546
+ assert.ok(VALID_ROUTE_TYPES.includes('internal'), 'must include internal');
547
+ assert.ok(VALID_ROUTE_TYPES.includes('external'), 'must include external');
548
+ assert.equal(VALID_ROUTE_TYPES.length, 2, 'must have exactly 2 route types');
549
+
550
+ assert.ok(Array.isArray(VALID_EXTERNAL_PROTOCOLS), 'VALID_EXTERNAL_PROTOCOLS must be an array');
551
+ assert.ok(VALID_EXTERNAL_PROTOCOLS.includes('openai'), 'must include openai');
552
+ assert.ok(VALID_EXTERNAL_PROTOCOLS.includes('anthropic'), 'must include anthropic');
553
+ assert.ok(VALID_EXTERNAL_PROTOCOLS.includes('bedrock'), 'must include bedrock');
554
+ });
555
+
556
+ // ---------------------------------------------------------------------------
557
+ // 29. ENVOY_AI_GATEWAY constants
558
+ // ---------------------------------------------------------------------------
559
+
560
+ test('ENVOY_AI_GATEWAY_API_GROUP and ENVOY_AI_GATEWAY_API_VERSION are exported', () => {
561
+ assert.equal(ENVOY_AI_GATEWAY_API_GROUP, 'aigateway.envoyproxy.io', 'API group must be aigateway.envoyproxy.io');
562
+ assert.equal(ENVOY_AI_GATEWAY_API_VERSION, 'v1alpha1', 'API version must be v1alpha1');
563
+ });
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // 30. validateModelRoute standalone export
567
+ // ---------------------------------------------------------------------------
568
+
569
+ test('validateModelRoute standalone export works', () => {
570
+ assert.equal(typeof validateModelRoute, 'function', 'must be a named export');
571
+ const route = makeExternalRoute('standalone');
572
+ const result = validateModelRoute(route);
573
+ assert.equal(result.valid, true, 'valid route must pass standalone validation');
574
+ });
575
+
576
+ // ---------------------------------------------------------------------------
577
+ // 31. KradleModelRoute resource creation via createResource
578
+ // ---------------------------------------------------------------------------
579
+
580
+ test('KradleModelRoute can be created via createResource', () => {
581
+ const resource = createResource('KradleModelRoute', { name: 'test-route', namespace: 'kradle-org-default' }, {
582
+ organizationRef: 'default',
583
+ modelName: 'gpt-4o',
584
+ routeType: 'external'
585
+ });
586
+
587
+ assert.equal(resource.kind, 'KradleModelRoute', 'kind must match');
588
+ assert.equal(resource.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
589
+ assert.equal(resource.spec.modelName, 'gpt-4o', 'modelName must be set');
590
+ assert.equal(resource.spec.routeType, 'external', 'routeType must be set');
591
+ });
592
+
593
+ // ---------------------------------------------------------------------------
594
+ // 32. reconcileRoutes — handles mix of valid and invalid routes
595
+ // ---------------------------------------------------------------------------
596
+
597
+ test('reconcileRoutes handles mix of valid and invalid routes', () => {
598
+ const controller = createModelRouteController();
599
+ const validRoute = makeExternalRoute('valid');
600
+ const invalidRoute = makeInternalRoute('invalid');
601
+ delete invalidRoute.spec.routeType;
602
+
603
+ const { conditions, resolvedRoutes } = controller.reconcileRoutes([validRoute, invalidRoute], []);
604
+
605
+ assert.equal(conditions.length, 2, 'must have two conditions');
606
+ assert.equal(conditions[0].status, 'True', 'first condition must be True');
607
+ assert.equal(conditions[1].status, 'False', 'second condition must be False');
608
+ assert.equal(resolvedRoutes.length, 1, 'must resolve only the valid route');
609
+ });
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // 33. generateEnvoyRouteManifest — internal route uses Service targetRef
613
+ // ---------------------------------------------------------------------------
614
+
615
+ test('generateEnvoyRouteManifest for internal route uses Service targetRef', () => {
616
+ const controller = createModelRouteController();
617
+ const route = makeInternalRoute('sklearn-iris');
618
+ const resolvedEndpoint = { endpoint: 'http://sklearn-iris.default.svc.cluster.local', protocol: 'v2', provider: 'kserve', modelId: 'sklearn-iris' };
619
+ const manifest = controller.generateEnvoyRouteManifest(route, resolvedEndpoint);
620
+
621
+ assert.equal(manifest.spec.targetRef.kind, 'Service', 'internal route must target a Service');
622
+ assert.equal(manifest.spec.targetRef.name, 'isvc-sklearn-iris', 'targetRef name must match inferenceServiceRef');
623
+ });
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // 34. generateEnvoyRouteManifest — external route uses ExternalBackend targetRef
627
+ // ---------------------------------------------------------------------------
628
+
629
+ test('generateEnvoyRouteManifest for external route uses ExternalBackend targetRef', () => {
630
+ const controller = createModelRouteController();
631
+ const route = makeExternalRoute('claude');
632
+ const resolvedEndpoint = { endpoint: 'https://api.anthropic.com/v1', protocol: 'anthropic', provider: 'anthropic', modelId: 'claude-3-opus' };
633
+ const manifest = controller.generateEnvoyRouteManifest(route, resolvedEndpoint);
634
+
635
+ assert.equal(manifest.spec.targetRef.kind, 'ExternalBackend', 'external route must target an ExternalBackend');
636
+ assert.ok(manifest.spec.targetRef.name.includes('anthropic'), 'targetRef name must include the provider');
637
+ });
638
+
639
+ // ---------------------------------------------------------------------------
640
+ // 35. resolveRoute — resolves internal from InferenceService status.address.url
641
+ // ---------------------------------------------------------------------------
642
+
643
+ test('resolveRoute resolves internal from InferenceService status.address.url', () => {
644
+ const controller = createModelRouteController();
645
+ const route = makeInternalRoute('addr', { inferenceServiceRef: 'my-svc' });
646
+ const isvc = {
647
+ kind: 'KradleInferenceService',
648
+ metadata: { name: 'my-svc', namespace: 'inference' },
649
+ spec: {},
650
+ status: { address: { url: 'http://my-svc.inference.svc.cluster.local' } }
651
+ };
652
+ const resolved = controller.resolveRoute(route, [isvc]);
653
+
654
+ assert.equal(resolved.endpoint, 'http://my-svc.inference.svc.cluster.local', 'must resolve from status.address.url');
655
+ });
656
+
657
+ // ---------------------------------------------------------------------------
658
+ // 36. validate — accumulates multiple errors
659
+ // ---------------------------------------------------------------------------
660
+
661
+ test('validate accumulates multiple errors at once', () => {
662
+ const controller = createModelRouteController();
663
+ const route = {
664
+ apiVersion: 'kradle.a5c.ai/v1alpha1',
665
+ kind: 'KradleModelRoute',
666
+ metadata: { name: 'multi-error', namespace: 'default', labels: {}, annotations: {} },
667
+ spec: {},
668
+ status: {}
669
+ };
670
+ const result = controller.validate(route);
671
+
672
+ assert.equal(result.valid, false, 'must fail validation');
673
+ assert.ok(result.errors.length >= 3, 'must have at least 3 errors (orgRef, modelName, routeType)');
674
+ assert.ok(result.errors.some(e => /organizationRef/i.test(e)), 'must mention organizationRef');
675
+ assert.ok(result.errors.some(e => /modelName/i.test(e)), 'must mention modelName');
676
+ assert.ok(result.errors.some(e => /routeType/i.test(e)), 'must mention routeType');
677
+ });
678
+
679
+ // ---------------------------------------------------------------------------
680
+ // 37. toModelRoute — sets source label to inference-service
681
+ // ---------------------------------------------------------------------------
682
+
683
+ test('toModelRoute sets source label to inference-service', () => {
684
+ const controller = createModelRouteController();
685
+ const isvc = {
686
+ metadata: { name: 'test-svc', namespace: 'default', labels: {} },
687
+ spec: { predictor: { modelFormat: { name: 'onnx' } } },
688
+ status: {}
689
+ };
690
+ const modelRoute = controller.toModelRoute(isvc, { endpoint: 'http://test-svc.default.svc.cluster.local' });
691
+
692
+ assert.equal(modelRoute.metadata.labels['kradle.a5c.ai/source'], 'inference-service', 'source label must be inference-service');
693
+ assert.equal(modelRoute.metadata.labels['kradle.a5c.ai/managed'], 'true', 'managed label must be true');
694
+ });
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // 38. listModelCatalog — empty routes returns empty catalog
698
+ // ---------------------------------------------------------------------------
699
+
700
+ test('listModelCatalog returns empty array for no routes', () => {
701
+ const controller = createModelRouteController();
702
+ const catalog = controller.listModelCatalog([], []);
703
+
704
+ assert.ok(Array.isArray(catalog), 'must return an array');
705
+ assert.equal(catalog.length, 0, 'must be empty for no routes');
706
+ });
707
+
708
+ // ---------------------------------------------------------------------------
709
+ // 39. resolveRoute — external route with missing external block returns null endpoint
710
+ // ---------------------------------------------------------------------------
711
+
712
+ test('resolveRoute for external route with empty external block returns null endpoint', () => {
713
+ const controller = createModelRouteController();
714
+ const route = makeExternalRoute('empty-ext');
715
+ route.spec.external = {};
716
+ const resolved = controller.resolveRoute(route, []);
717
+
718
+ assert.equal(resolved.endpoint, null, 'endpoint must be null when external.endpoint is missing');
719
+ assert.equal(resolved.provider, 'unknown', 'provider must be unknown when external.provider is missing');
720
+ });
721
+
722
+ // ---------------------------------------------------------------------------
723
+ // 40. generateEnvoyRouteManifest — includes timeout when specified
724
+ // ---------------------------------------------------------------------------
725
+
726
+ test('generateEnvoyRouteManifest includes timeout when specified', () => {
727
+ const controller = createModelRouteController();
728
+ const route = makeExternalRoute('gpt4-timeout', { timeout: '30s' });
729
+ const resolvedEndpoint = { endpoint: 'https://api.openai.com/v1', protocol: 'openai', provider: 'openai', modelId: 'gpt-4o' };
730
+ const manifest = controller.generateEnvoyRouteManifest(route, resolvedEndpoint);
731
+
732
+ assert.equal(manifest.spec.timeout, '30s', 'timeout must be included');
733
+ });