@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.
- package/Dockerfile +31 -0
- package/README.md +187 -0
- package/bin/kradle-demo.mjs +23 -0
- package/bin/kradle-server.mjs +14 -0
- package/dist/kradle-controller-ui.json +3482 -0
- package/dist/kradle-lifecycle.json +201 -0
- package/dist/kradle-runtime-snapshot.json +3125 -0
- package/dist/kradle-summary.json +724 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/gaps-agent-mux-to-kradle-crds.md +298 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/architecture-v2.md +2759 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/crd-behaviors-and-relationships.md +3926 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/integration-and-design-decisions.md +1530 -0
- package/docs/kradle-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/openapi.yaml +1291 -0
- package/docs/product-requirements.md +62 -0
- package/docs/requirements-v2.md +235 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/sdk-api-reference.md +1108 -0
- package/docs/system-requirements.md +90 -0
- package/docs/system-spec-v2.md +1230 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/todos.md +4 -0
- package/docs/user-stories.md +78 -0
- package/docs/web-console-spec.md +533 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +66 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +95 -0
- package/scripts/validate-ui.mjs +305 -0
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +170 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +549 -0
- package/src/agent-gateway-config-controller.js +147 -0
- package/src/agent-identity-migration.js +115 -0
- package/src/agent-memory-controller.js +357 -0
- package/src/agent-memory-import.js +327 -0
- package/src/agent-memory-query.js +292 -0
- package/src/agent-memory-repository-source-controller.js +255 -0
- package/src/agent-mux-client.js +589 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-persona-controller.js +135 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-prompt-composition.js +55 -0
- package/src/agent-provider-config-controller.js +151 -0
- package/src/agent-secret-config-grant-controller.js +282 -0
- package/src/agent-session-transcript-controller.js +189 -0
- package/src/agent-stack-controller.js +421 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +387 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +621 -0
- package/src/argocd-gitops.js +43 -0
- package/src/artifact-registry-controller.js +542 -0
- package/src/assistant-runtime.js +284 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +310 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +112 -0
- package/src/controller-ui.js +620 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +397 -0
- package/src/external/conflict-controller.js +225 -0
- package/src/external/github/auth.js +96 -0
- package/src/external/github/cicd.js +180 -0
- package/src/external/github/git-forge.js +240 -0
- package/src/external/github/index.js +144 -0
- package/src/external/github/issue-tracking.js +163 -0
- package/src/external/provider-adapter.js +161 -0
- package/src/external/provider-resource-factory.js +221 -0
- package/src/external/sync-controller.js +235 -0
- package/src/external/webhook-controller.js +144 -0
- package/src/external/write-controller.js +283 -0
- package/src/gitea-backend.js +131 -0
- package/src/gitea-service.js +173 -0
- package/src/handoff.js +98 -0
- package/src/health-probes.js +134 -0
- package/src/hooks-events.js +63 -0
- package/src/hooks-lifecycle.js +117 -0
- package/src/http-server.js +409 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +71 -0
- package/src/jitsi-agent-bridge.js +141 -0
- package/src/jitsi-meeting-controller.js +291 -0
- package/src/jitsi-sync-controller.js +198 -0
- package/src/kradle-inference-service-controller.js +246 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +904 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/model-route-controller.js +364 -0
- package/src/notification-controller.js +178 -0
- package/src/operations.js +112 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +282 -0
- package/src/runner-controller.js +272 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/snapshot-cache.js +157 -0
- package/src/virtual-model-controller.js +538 -0
- package/src/virtual-model-hook-bridge.js +200 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +679 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-identity-migration.test.js +87 -0
- package/tests/agent-memory-controller.test.js +461 -0
- package/tests/agent-memory-import-snapshot.test.js +477 -0
- package/tests/agent-memory-query.test.js +404 -0
- package/tests/agent-memory-repository-source.test.js +514 -0
- package/tests/agent-mux-client.test.js +389 -0
- package/tests/agent-mux-integration.test.js +971 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-persona-controller.test.js +127 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-prompt-composition.test.js +76 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +303 -0
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-stack-controller.test.js +283 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -0
- package/tests/agent-trigger-controller.test.js +271 -0
- package/tests/agent-trigger-routes.test.js +190 -0
- package/tests/agent-trigger-sources.test.js +245 -0
- package/tests/agent-workspace-controller.test.js +181 -0
- package/tests/agent-writeback.test.js +292 -0
- package/tests/approval-persistence.test.js +171 -0
- package/tests/artifact-registry.test.js +511 -0
- package/tests/assistant-runtime.test.js +506 -0
- package/tests/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/controller-client.test.js +133 -0
- package/tests/deployment.test.js +527 -0
- package/tests/e2e/lifecycle.test.js +120 -0
- package/tests/event-bus-integration.test.js +355 -0
- package/tests/external-github-forge.test.js +560 -0
- package/tests/external-github-issues-cicd.test.js +520 -0
- package/tests/external-integration.test.js +470 -0
- package/tests/external-persistence.test.js +415 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +223 -0
- package/tests/external-webhook-sync.test.js +287 -0
- package/tests/external-write-conflict.test.js +353 -0
- package/tests/gitea-service.test.js +253 -0
- package/tests/health-check-real.test.js +165 -0
- package/tests/health-probes.test.js +90 -0
- package/tests/hooks-lifecycle.test.js +364 -0
- package/tests/integration/full-flow.test.js +266 -0
- package/tests/jitsi-agent-bridge.test.js +119 -0
- package/tests/jitsi-helm-integration.test.js +77 -0
- package/tests/jitsi-meeting-controller.test.js +170 -0
- package/tests/jitsi-resource-model.test.js +73 -0
- package/tests/jitsi-sync-controller.test.js +112 -0
- package/tests/kradle-inference-service.test.js +689 -0
- package/tests/kradle.test.js +779 -0
- package/tests/memory-search-wiring.test.js +270 -0
- package/tests/model-route-controller.test.js +733 -0
- package/tests/notification-controller.test.js +196 -0
- package/tests/notification-integration.test.js +179 -0
- package/tests/org-scoping.test.js +687 -0
- package/tests/runner-controller.test.js +327 -0
- package/tests/runner-integration.test.js +231 -0
- package/tests/session-cookie-hmac.test.js +151 -0
- package/tests/snapshot-performance.test.js +315 -0
- package/tests/sse-events.test.js +107 -0
- package/tests/virtual-model-controller.test.js +877 -0
- package/tests/virtual-model-hook-bridge.test.js +384 -0
- package/tests/webhook-trigger.test.js +198 -0
- package/tests/workspace-volumes.test.js +312 -0
- 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
|
+
});
|