@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,689 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
createInferenceServiceController,
|
|
5
|
+
validateKradleInferenceService,
|
|
6
|
+
createResource,
|
|
7
|
+
KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY,
|
|
8
|
+
KSERVE_API_GROUP,
|
|
9
|
+
KSERVE_API_VERSION,
|
|
10
|
+
SUPPORTED_MODEL_FORMATS,
|
|
11
|
+
INFERENCE_PROTOCOLS
|
|
12
|
+
} from '../src/index.js';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// KServe Inference Service Controller Tests
|
|
16
|
+
//
|
|
17
|
+
// A KradleInferenceService wraps KServe InferenceService resources for model
|
|
18
|
+
// inference with endpoint discovery, V1/V2 protocol support, provider config
|
|
19
|
+
// bridge, health checks, and model metadata.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
function makeInferenceService(name, overrides = {}) {
|
|
23
|
+
return createResource('KradleInferenceService', { name, namespace: 'kradle-org-default' }, {
|
|
24
|
+
organizationRef: 'default',
|
|
25
|
+
modelFormat: 'sklearn',
|
|
26
|
+
storageUri: 's3://models/sklearn/iris',
|
|
27
|
+
...overrides
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// 1. Factory and shape
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
test('createInferenceServiceController returns controller with expected methods', () => {
|
|
36
|
+
const controller = createInferenceServiceController();
|
|
37
|
+
assert.ok(controller, 'controller must be truthy');
|
|
38
|
+
assert.equal(typeof controller.validate, 'function', 'controller must expose validate');
|
|
39
|
+
assert.equal(typeof controller.generateKServeManifest, 'function', 'controller must expose generateKServeManifest');
|
|
40
|
+
assert.equal(typeof controller.resolveEndpoint, 'function', 'controller must expose resolveEndpoint');
|
|
41
|
+
assert.equal(typeof controller.toProviderConfig, 'function', 'controller must expose toProviderConfig');
|
|
42
|
+
assert.equal(typeof controller.infer, 'function', 'controller must expose infer');
|
|
43
|
+
assert.equal(typeof controller.checkHealth, 'function', 'controller must expose checkHealth');
|
|
44
|
+
assert.equal(typeof controller.getModelMetadata, 'function', 'controller must expose getModelMetadata');
|
|
45
|
+
assert.equal(typeof controller.listModels, 'function', 'controller must expose listModels');
|
|
46
|
+
assert.equal(controller.role, 'kradle-inference-service-controller', 'controller must declare its role');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// 2. validate — accepts valid sklearn InferenceService
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
test('validate accepts valid sklearn InferenceService', () => {
|
|
54
|
+
const controller = createInferenceServiceController();
|
|
55
|
+
const resource = makeInferenceService('sklearn-iris');
|
|
56
|
+
const result = controller.validate(resource);
|
|
57
|
+
|
|
58
|
+
assert.equal(result.valid, true, 'valid sklearn resource must pass validation');
|
|
59
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// 3. validate — accepts valid pytorch with GPU resources
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
test('validate accepts valid pytorch with GPU resources', () => {
|
|
67
|
+
const controller = createInferenceServiceController();
|
|
68
|
+
const resource = makeInferenceService('pytorch-resnet', {
|
|
69
|
+
modelFormat: 'pytorch',
|
|
70
|
+
storageUri: 's3://models/pytorch/resnet50',
|
|
71
|
+
resources: { limits: { cpu: '4', memory: '16Gi', 'nvidia.com/gpu': '1' } }
|
|
72
|
+
});
|
|
73
|
+
const result = controller.validate(resource);
|
|
74
|
+
|
|
75
|
+
assert.equal(result.valid, true, 'pytorch resource with GPU must pass validation');
|
|
76
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// 4. validate — rejects unknown model format
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
test('validate rejects unknown model format', () => {
|
|
84
|
+
const controller = createInferenceServiceController();
|
|
85
|
+
const resource = makeInferenceService('bad-format', { modelFormat: 'caffe' });
|
|
86
|
+
const result = controller.validate(resource);
|
|
87
|
+
|
|
88
|
+
assert.equal(result.valid, false, 'unknown model format must fail validation');
|
|
89
|
+
assert.ok(result.errors.length > 0, 'errors must not be empty');
|
|
90
|
+
assert.ok(result.errors.some(e => /modelFormat/i.test(e)), 'error must mention modelFormat');
|
|
91
|
+
assert.ok(result.errors.some(e => SUPPORTED_MODEL_FORMATS.some(f => e.includes(f))), 'error must enumerate valid formats');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// 5. validate — rejects missing storageUri
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
test('validate rejects missing storageUri', () => {
|
|
99
|
+
const controller = createInferenceServiceController();
|
|
100
|
+
const resource = makeInferenceService('no-storage');
|
|
101
|
+
delete resource.spec.storageUri;
|
|
102
|
+
const result = controller.validate(resource);
|
|
103
|
+
|
|
104
|
+
assert.equal(result.valid, false, 'missing storageUri must fail validation');
|
|
105
|
+
assert.ok(result.errors.some(e => /storageUri/i.test(e)), 'error must mention storageUri');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// 6. validate — rejects invalid storageUri scheme
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
test('validate rejects invalid storageUri scheme', () => {
|
|
113
|
+
const controller = createInferenceServiceController();
|
|
114
|
+
const resource = makeInferenceService('bad-uri', { storageUri: 'ftp://models/bad' });
|
|
115
|
+
const result = controller.validate(resource);
|
|
116
|
+
|
|
117
|
+
assert.equal(result.valid, false, 'invalid storageUri scheme must fail validation');
|
|
118
|
+
assert.ok(result.errors.some(e => /storageUri/i.test(e)), 'error must mention storageUri');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// 7. validate — rejects null resource
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
test('validate rejects null resource', () => {
|
|
126
|
+
const controller = createInferenceServiceController();
|
|
127
|
+
const result = controller.validate(null);
|
|
128
|
+
|
|
129
|
+
assert.equal(result.valid, false, 'null resource must fail validation');
|
|
130
|
+
assert.ok(result.errors.some(e => /null|undefined/i.test(e)), 'error must mention null or undefined');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// 8. generateKServeManifest — produces valid KServe spec
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
test('generateKServeManifest produces valid KServe spec', () => {
|
|
138
|
+
const controller = createInferenceServiceController();
|
|
139
|
+
const resource = makeInferenceService('sklearn-iris', {
|
|
140
|
+
modelFormat: 'sklearn',
|
|
141
|
+
storageUri: 's3://models/sklearn/iris'
|
|
142
|
+
});
|
|
143
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
144
|
+
|
|
145
|
+
assert.equal(manifest.apiVersion, 'serving.kserve.io/v1beta1', 'apiVersion must be serving.kserve.io/v1beta1');
|
|
146
|
+
assert.equal(manifest.kind, 'InferenceService', 'kind must be InferenceService');
|
|
147
|
+
assert.equal(manifest.metadata.name, 'sklearn-iris', 'name must match');
|
|
148
|
+
assert.equal(manifest.metadata.labels['kradle.a5c.ai/managed'], 'true', 'managed label must be set');
|
|
149
|
+
assert.equal(manifest.metadata.labels['kradle.a5c.ai/org'], 'default', 'org label must be set');
|
|
150
|
+
assert.equal(manifest.spec.predictor.modelFormat.name, 'sklearn', 'model format name must match');
|
|
151
|
+
assert.equal(manifest.spec.predictor.storageUri, 's3://models/sklearn/iris', 'storageUri must match');
|
|
152
|
+
assert.ok(manifest.spec.predictor.resources, 'resources must be present (defaults)');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// 9. generateKServeManifest — includes transformer when specified
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
test('generateKServeManifest includes transformer when specified', () => {
|
|
160
|
+
const controller = createInferenceServiceController();
|
|
161
|
+
const resource = makeInferenceService('with-transformer', {
|
|
162
|
+
transformer: { containers: [{ image: 'custom-transformer:latest' }] }
|
|
163
|
+
});
|
|
164
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
165
|
+
|
|
166
|
+
assert.ok(manifest.spec.transformer, 'transformer must be present in manifest');
|
|
167
|
+
assert.deepEqual(manifest.spec.transformer, resource.spec.transformer, 'transformer must match spec');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// 10. generateKServeManifest — includes explainer when specified
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
test('generateKServeManifest includes explainer when specified', () => {
|
|
175
|
+
const controller = createInferenceServiceController();
|
|
176
|
+
const resource = makeInferenceService('with-explainer', {
|
|
177
|
+
explainer: { containers: [{ image: 'alibi-explainer:latest' }] }
|
|
178
|
+
});
|
|
179
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
180
|
+
|
|
181
|
+
assert.ok(manifest.spec.explainer, 'explainer must be present in manifest');
|
|
182
|
+
assert.deepEqual(manifest.spec.explainer, resource.spec.explainer, 'explainer must match spec');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 11. generateKServeManifest — omits transformer and explainer when not specified
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test('generateKServeManifest omits transformer and explainer when not specified', () => {
|
|
190
|
+
const controller = createInferenceServiceController();
|
|
191
|
+
const resource = makeInferenceService('plain-predictor');
|
|
192
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
193
|
+
|
|
194
|
+
assert.equal(manifest.spec.transformer, undefined, 'transformer must not be present');
|
|
195
|
+
assert.equal(manifest.spec.explainer, undefined, 'explainer must not be present');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
// 12. resolveEndpoint — extracts URL from status
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
test('resolveEndpoint extracts URL from status', () => {
|
|
203
|
+
const controller = createInferenceServiceController();
|
|
204
|
+
const isvc = {
|
|
205
|
+
metadata: { name: 'my-model', namespace: 'inference' },
|
|
206
|
+
status: { url: 'https://my-model.inference.example.com' }
|
|
207
|
+
};
|
|
208
|
+
const endpoint = controller.resolveEndpoint(isvc);
|
|
209
|
+
|
|
210
|
+
assert.equal(endpoint, 'https://my-model.inference.example.com', 'must return status URL');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// 13. resolveEndpoint — extracts URL from status.address.url
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
test('resolveEndpoint extracts URL from status.address.url', () => {
|
|
218
|
+
const controller = createInferenceServiceController();
|
|
219
|
+
const isvc = {
|
|
220
|
+
metadata: { name: 'my-model', namespace: 'inference' },
|
|
221
|
+
status: { address: { url: 'http://my-model.inference.svc.cluster.local' } }
|
|
222
|
+
};
|
|
223
|
+
const endpoint = controller.resolveEndpoint(isvc);
|
|
224
|
+
|
|
225
|
+
assert.equal(endpoint, 'http://my-model.inference.svc.cluster.local', 'must return address URL');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// 14. resolveEndpoint — generates cluster-internal URL as fallback
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
test('resolveEndpoint generates cluster-internal URL as fallback', () => {
|
|
233
|
+
const controller = createInferenceServiceController();
|
|
234
|
+
const isvc = {
|
|
235
|
+
metadata: { name: 'my-model', namespace: 'inference' },
|
|
236
|
+
status: {}
|
|
237
|
+
};
|
|
238
|
+
const endpoint = controller.resolveEndpoint(isvc);
|
|
239
|
+
|
|
240
|
+
assert.equal(endpoint, 'http://my-model.inference.svc.cluster.local', 'must generate cluster-internal URL');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// 15. toProviderConfig — creates valid provider with kserve type
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
test('toProviderConfig creates valid provider config with kserve type', () => {
|
|
248
|
+
const controller = createInferenceServiceController();
|
|
249
|
+
const resource = makeInferenceService('sklearn-iris');
|
|
250
|
+
const config = controller.toProviderConfig(resource, 'http://sklearn-iris.default.svc.cluster.local');
|
|
251
|
+
|
|
252
|
+
assert.equal(config.providerType, 'kserve', 'providerType must be kserve');
|
|
253
|
+
assert.equal(config.endpoint, 'http://sklearn-iris.default.svc.cluster.local', 'endpoint must match');
|
|
254
|
+
assert.ok(config.modelCatalog, 'modelCatalog must be present');
|
|
255
|
+
assert.ok(Array.isArray(config.modelCatalog), 'modelCatalog must be an array');
|
|
256
|
+
assert.ok(config.rateLimits, 'rateLimits must be present');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// 16. toProviderConfig — includes v2 protocol by default
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
test('toProviderConfig includes v2 protocol by default', () => {
|
|
264
|
+
const controller = createInferenceServiceController();
|
|
265
|
+
const resource = makeInferenceService('default-protocol');
|
|
266
|
+
const config = controller.toProviderConfig(resource, 'http://localhost:8080');
|
|
267
|
+
|
|
268
|
+
assert.equal(config.protocol, 'v2', 'default protocol must be v2');
|
|
269
|
+
assert.equal(config.featureFlags.v2_protocol, true, 'v2_protocol flag must be true');
|
|
270
|
+
assert.equal(config.featureFlags.v1_protocol, false, 'v1_protocol flag must be false');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// 17. toProviderConfig — includes explainability flag when explainer present
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
test('toProviderConfig includes explainability flag when explainer present', () => {
|
|
278
|
+
const controller = createInferenceServiceController();
|
|
279
|
+
const resource = makeInferenceService('with-explainer', {
|
|
280
|
+
explainer: { containers: [{ image: 'alibi:latest' }] }
|
|
281
|
+
});
|
|
282
|
+
const config = controller.toProviderConfig(resource, 'http://localhost:8080');
|
|
283
|
+
|
|
284
|
+
assert.equal(config.featureFlags.explainability, true, 'explainability flag must be true when explainer present');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
// 18. infer — sends V2 protocol request
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
test('infer sends V2 protocol request', async () => {
|
|
292
|
+
const controller = createInferenceServiceController();
|
|
293
|
+
let capturedUrl, capturedBody;
|
|
294
|
+
const mockFetch = async (url, opts) => {
|
|
295
|
+
capturedUrl = url;
|
|
296
|
+
capturedBody = JSON.parse(opts.body);
|
|
297
|
+
return { ok: true, json: async () => ({ outputs: [{ data: [1] }] }) };
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const inputs = [{ name: 'input', shape: [1, 4], datatype: 'FP32', data: [5.1, 3.5, 1.4, 0.2] }];
|
|
301
|
+
const result = await controller.infer('http://localhost:8080', 'iris-model', inputs, { fetchImpl: mockFetch });
|
|
302
|
+
|
|
303
|
+
assert.ok(capturedUrl.includes('/v2/models/iris-model/infer'), 'must use V2 infer path');
|
|
304
|
+
assert.ok(capturedBody.inputs, 'V2 body must contain inputs array');
|
|
305
|
+
assert.equal(capturedBody.inputs[0].name, 'input', 'input name must match');
|
|
306
|
+
assert.ok(result.outputs, 'result must contain outputs');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// 19. infer — sends V1 protocol request when specified
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
test('infer sends V1 protocol request when specified', async () => {
|
|
314
|
+
const controller = createInferenceServiceController();
|
|
315
|
+
let capturedUrl, capturedBody;
|
|
316
|
+
const mockFetch = async (url, opts) => {
|
|
317
|
+
capturedUrl = url;
|
|
318
|
+
capturedBody = JSON.parse(opts.body);
|
|
319
|
+
return { ok: true, json: async () => ({ predictions: [[1]] }) };
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const inputs = [{ name: 'input', data: [5.1, 3.5, 1.4, 0.2] }];
|
|
323
|
+
const result = await controller.infer('http://localhost:8080', 'iris-model', inputs, { protocol: 'v1', fetchImpl: mockFetch });
|
|
324
|
+
|
|
325
|
+
assert.ok(capturedUrl.includes('/v1/models/iris-model:predict'), 'must use V1 predict path');
|
|
326
|
+
assert.ok(capturedBody.instances, 'V1 body must contain instances array');
|
|
327
|
+
assert.ok(result.predictions, 'result must contain predictions');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// 20. infer — includes auth token when provided
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
test('infer includes auth token when provided', async () => {
|
|
335
|
+
const controller = createInferenceServiceController();
|
|
336
|
+
let capturedHeaders;
|
|
337
|
+
const mockFetch = async (url, opts) => {
|
|
338
|
+
capturedHeaders = opts.headers;
|
|
339
|
+
return { ok: true, json: async () => ({ outputs: [] }) };
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
await controller.infer('http://localhost:8080', 'model', [{ name: 'x', shape: [1], datatype: 'FP32', data: [1] }], {
|
|
343
|
+
authToken: 'test-token-123',
|
|
344
|
+
fetchImpl: mockFetch
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
assert.equal(capturedHeaders.Authorization, 'Bearer test-token-123', 'must include Authorization header');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
// 21. infer — throws on non-ok response
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
|
|
354
|
+
test('infer throws on non-ok response', async () => {
|
|
355
|
+
const controller = createInferenceServiceController();
|
|
356
|
+
const mockFetch = async () => ({
|
|
357
|
+
ok: false,
|
|
358
|
+
status: 500,
|
|
359
|
+
text: async () => 'Internal Server Error'
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await assert.rejects(
|
|
363
|
+
() => controller.infer('http://localhost:8080', 'model', [{ name: 'x', shape: [1], datatype: 'FP32', data: [1] }], { fetchImpl: mockFetch }),
|
|
364
|
+
/Inference failed: 500/,
|
|
365
|
+
'must throw on non-ok response'
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// 22. checkHealth — returns healthy for 200
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
test('checkHealth returns healthy for 200', async () => {
|
|
374
|
+
const controller = createInferenceServiceController();
|
|
375
|
+
const mockFetch = async () => ({ ok: true, status: 200 });
|
|
376
|
+
const health = await controller.checkHealth('http://localhost:8080', { fetchImpl: mockFetch });
|
|
377
|
+
|
|
378
|
+
assert.equal(health.healthy, true, 'must be healthy');
|
|
379
|
+
assert.equal(health.status, 200, 'status must be 200');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------
|
|
383
|
+
// 23. checkHealth — returns unhealthy for timeout
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
test('checkHealth returns unhealthy for timeout', async () => {
|
|
387
|
+
const controller = createInferenceServiceController();
|
|
388
|
+
const mockFetch = async () => { throw new Error('timeout exceeded'); };
|
|
389
|
+
const health = await controller.checkHealth('http://localhost:8080', { fetchImpl: mockFetch });
|
|
390
|
+
|
|
391
|
+
assert.equal(health.healthy, false, 'must be unhealthy');
|
|
392
|
+
assert.ok(health.error, 'error must be present');
|
|
393
|
+
assert.ok(health.error.includes('timeout'), 'error must mention timeout');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
// 24. getModelMetadata — returns model schema
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
test('getModelMetadata returns model schema', async () => {
|
|
401
|
+
const controller = createInferenceServiceController();
|
|
402
|
+
const mockMetadata = { name: 'iris', versions: ['1'], platform: 'sklearn', inputs: [], outputs: [] };
|
|
403
|
+
const mockFetch = async () => ({ ok: true, json: async () => mockMetadata });
|
|
404
|
+
const metadata = await controller.getModelMetadata('http://localhost:8080', 'iris', { fetchImpl: mockFetch });
|
|
405
|
+
|
|
406
|
+
assert.deepEqual(metadata, mockMetadata, 'must return model metadata');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// 25. getModelMetadata — returns null for not found
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
test('getModelMetadata returns null for not found', async () => {
|
|
414
|
+
const controller = createInferenceServiceController();
|
|
415
|
+
const mockFetch = async () => ({ ok: false, status: 404 });
|
|
416
|
+
const metadata = await controller.getModelMetadata('http://localhost:8080', 'missing', { fetchImpl: mockFetch });
|
|
417
|
+
|
|
418
|
+
assert.equal(metadata, null, 'must return null for missing model');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// 26. listModels — returns available models
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
test('listModels returns available models', async () => {
|
|
426
|
+
const controller = createInferenceServiceController();
|
|
427
|
+
const mockFetch = async () => ({ ok: true, json: async () => ({ models: ['iris', 'resnet'] }) });
|
|
428
|
+
const models = await controller.listModels('http://localhost:8080', { fetchImpl: mockFetch });
|
|
429
|
+
|
|
430
|
+
assert.deepEqual(models, ['iris', 'resnet'], 'must return model list');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// 27. listModels — returns empty array on error
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
test('listModels returns empty array on error', async () => {
|
|
438
|
+
const controller = createInferenceServiceController();
|
|
439
|
+
const mockFetch = async () => { throw new Error('connection refused'); };
|
|
440
|
+
const models = await controller.listModels('http://localhost:8080', { fetchImpl: mockFetch });
|
|
441
|
+
|
|
442
|
+
assert.deepEqual(models, [], 'must return empty array on error');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// 28. SUPPORTED_MODEL_FORMATS includes all expected formats
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
test('SUPPORTED_MODEL_FORMATS includes all expected formats', () => {
|
|
450
|
+
const expected = ['sklearn', 'xgboost', 'lightgbm', 'tensorflow', 'pytorch', 'onnx', 'triton', 'huggingface', 'custom'];
|
|
451
|
+
for (const format of expected) {
|
|
452
|
+
assert.ok(SUPPORTED_MODEL_FORMATS.includes(format), `must include "${format}"`);
|
|
453
|
+
}
|
|
454
|
+
assert.equal(SUPPORTED_MODEL_FORMATS.length, expected.length, 'must have exactly the expected number of formats');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// 29. KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY exported with correct role
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
test('KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY is exported with correct role', () => {
|
|
462
|
+
assert.ok(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
|
|
463
|
+
assert.equal(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY.role, 'kradle-inference-service-controller', 'role must match');
|
|
464
|
+
assert.ok(Array.isArray(KRADLE_INFERENCE_SERVICE_CONTROLLER_BOUNDARY.owns), 'must declare owned concerns');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
// 30. KSERVE_API_GROUP and KSERVE_API_VERSION exports
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
|
|
471
|
+
test('KSERVE_API_GROUP and KSERVE_API_VERSION are exported correctly', () => {
|
|
472
|
+
assert.equal(KSERVE_API_GROUP, 'serving.kserve.io', 'KSERVE_API_GROUP must be serving.kserve.io');
|
|
473
|
+
assert.equal(KSERVE_API_VERSION, 'v1beta1', 'KSERVE_API_VERSION must be v1beta1');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// 31. INFERENCE_PROTOCOLS exports
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
test('INFERENCE_PROTOCOLS exports V1 and V2', () => {
|
|
481
|
+
assert.equal(INFERENCE_PROTOCOLS.V1, 'v1', 'V1 must be v1');
|
|
482
|
+
assert.equal(INFERENCE_PROTOCOLS.V2, 'v2', 'V2 must be v2');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// 32. validateKradleInferenceService standalone export
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
test('validateKradleInferenceService standalone export works', () => {
|
|
490
|
+
assert.equal(typeof validateKradleInferenceService, 'function', 'must be a named export');
|
|
491
|
+
const resource = makeInferenceService('standalone-test');
|
|
492
|
+
const result = validateKradleInferenceService(resource);
|
|
493
|
+
assert.equal(result.valid, true, 'valid resource must pass standalone validation');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
// 33. validate — accepts all supported model formats
|
|
498
|
+
// ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
test('validate accepts all supported model formats', () => {
|
|
501
|
+
const controller = createInferenceServiceController();
|
|
502
|
+
for (const format of SUPPORTED_MODEL_FORMATS) {
|
|
503
|
+
const resource = makeInferenceService(`${format}-service`, { modelFormat: format });
|
|
504
|
+
const result = controller.validate(resource);
|
|
505
|
+
assert.equal(result.valid, true, `modelFormat "${format}" must pass validation`);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// 34. toProviderConfig — v1 protocol flags
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
|
|
513
|
+
test('toProviderConfig respects v1 protocol setting', () => {
|
|
514
|
+
const controller = createInferenceServiceController();
|
|
515
|
+
const resource = makeInferenceService('v1-model', { inferenceProtocol: 'v1' });
|
|
516
|
+
const config = controller.toProviderConfig(resource, 'http://localhost:8080');
|
|
517
|
+
|
|
518
|
+
assert.equal(config.protocol, 'v1', 'protocol must be v1');
|
|
519
|
+
assert.equal(config.featureFlags.v1_protocol, true, 'v1_protocol flag must be true');
|
|
520
|
+
assert.equal(config.featureFlags.v2_protocol, false, 'v2_protocol flag must be false');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// 35. generateKServeManifest — includes runtime when specified
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
test('generateKServeManifest includes runtime when specified', () => {
|
|
528
|
+
const controller = createInferenceServiceController();
|
|
529
|
+
const resource = makeInferenceService('with-runtime', { runtime: 'kserve-tritonserver' });
|
|
530
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
531
|
+
|
|
532
|
+
assert.equal(manifest.spec.predictor.runtime, 'kserve-tritonserver', 'runtime must be set in predictor');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
// 36. generateKServeManifest — omits runtime when not specified
|
|
537
|
+
// ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
test('generateKServeManifest omits runtime when not specified', () => {
|
|
540
|
+
const controller = createInferenceServiceController();
|
|
541
|
+
const resource = makeInferenceService('no-runtime');
|
|
542
|
+
const manifest = controller.generateKServeManifest(resource);
|
|
543
|
+
|
|
544
|
+
assert.equal(manifest.spec.predictor.runtime, undefined, 'runtime must not be present');
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// 37. getSupportedModelFormats — returns copy of formats
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
test('getSupportedModelFormats returns supported model formats', () => {
|
|
552
|
+
const controller = createInferenceServiceController();
|
|
553
|
+
const formats = controller.getSupportedModelFormats();
|
|
554
|
+
|
|
555
|
+
assert.ok(Array.isArray(formats), 'must return an array');
|
|
556
|
+
assert.equal(formats.length, SUPPORTED_MODEL_FORMATS.length, 'must contain all formats');
|
|
557
|
+
assert.ok(formats.includes('sklearn'), 'must include sklearn');
|
|
558
|
+
assert.ok(formats.includes('pytorch'), 'must include pytorch');
|
|
559
|
+
// Verify it returns a copy, not the original
|
|
560
|
+
formats.push('extra');
|
|
561
|
+
assert.equal(SUPPORTED_MODEL_FORMATS.length, 9, 'must not modify original array');
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ---------------------------------------------------------------------------
|
|
565
|
+
// 38. validate — rejects missing organizationRef
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
|
|
568
|
+
test('validate rejects missing organizationRef', () => {
|
|
569
|
+
const controller = createInferenceServiceController();
|
|
570
|
+
const resource = {
|
|
571
|
+
apiVersion: 'kradle.a5c.ai/v1alpha1',
|
|
572
|
+
kind: 'KradleInferenceService',
|
|
573
|
+
metadata: { name: 'no-org', namespace: 'default', labels: {}, annotations: {} },
|
|
574
|
+
spec: { modelFormat: 'sklearn', storageUri: 's3://models/test' },
|
|
575
|
+
status: {}
|
|
576
|
+
};
|
|
577
|
+
const result = controller.validate(resource);
|
|
578
|
+
|
|
579
|
+
assert.equal(result.valid, false, 'missing organizationRef must fail');
|
|
580
|
+
assert.ok(result.errors.some(e => /organizationRef/i.test(e)), 'error must mention organizationRef');
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
// 39. validate — accepts pvc:// storageUri
|
|
585
|
+
// ---------------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
test('validate accepts pvc:// storageUri', () => {
|
|
588
|
+
const controller = createInferenceServiceController();
|
|
589
|
+
const resource = makeInferenceService('pvc-model', { storageUri: 'pvc://model-volume/path' });
|
|
590
|
+
const result = controller.validate(resource);
|
|
591
|
+
assert.equal(result.valid, true, 'pvc:// URI must be valid');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// ---------------------------------------------------------------------------
|
|
595
|
+
// 40. validate — accepts https:// storageUri
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
test('validate accepts https:// storageUri', () => {
|
|
599
|
+
const controller = createInferenceServiceController();
|
|
600
|
+
const resource = makeInferenceService('https-model', { storageUri: 'https://huggingface.co/models/bert' });
|
|
601
|
+
const result = controller.validate(resource);
|
|
602
|
+
assert.equal(result.valid, true, 'https:// URI must be valid');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
// 41. validate — accepts gs:// storageUri
|
|
607
|
+
// ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
test('validate accepts gs:// storageUri', () => {
|
|
610
|
+
const controller = createInferenceServiceController();
|
|
611
|
+
const resource = makeInferenceService('gcs-model', { storageUri: 'gs://bucket/models/sklearn/iris' });
|
|
612
|
+
const result = controller.validate(resource);
|
|
613
|
+
assert.equal(result.valid, true, 'gs:// URI must be valid');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
// 42. resolveEndpoint — uses default namespace when not specified
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
|
|
620
|
+
test('resolveEndpoint uses default namespace when not specified', () => {
|
|
621
|
+
const controller = createInferenceServiceController();
|
|
622
|
+
const isvc = {
|
|
623
|
+
metadata: { name: 'my-model' },
|
|
624
|
+
status: {}
|
|
625
|
+
};
|
|
626
|
+
const endpoint = controller.resolveEndpoint(isvc);
|
|
627
|
+
assert.equal(endpoint, 'http://my-model.default.svc.cluster.local', 'must use default namespace');
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ---------------------------------------------------------------------------
|
|
631
|
+
// 43. toProviderConfig — includes streaming feature flag from spec
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
test('toProviderConfig includes streaming feature flag from spec', () => {
|
|
635
|
+
const controller = createInferenceServiceController();
|
|
636
|
+
const resource = makeInferenceService('streaming-model', {
|
|
637
|
+
featureFlags: { streaming: true, batchInference: true }
|
|
638
|
+
});
|
|
639
|
+
const config = controller.toProviderConfig(resource, 'http://localhost:8080');
|
|
640
|
+
|
|
641
|
+
assert.equal(config.featureFlags.streaming, true, 'streaming must be true');
|
|
642
|
+
assert.equal(config.featureFlags.batch_inference, true, 'batch_inference must be true');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// ---------------------------------------------------------------------------
|
|
646
|
+
// 44. toProviderConfig — uses custom model catalog when provided
|
|
647
|
+
// ---------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
test('toProviderConfig uses custom model catalog when provided', () => {
|
|
650
|
+
const controller = createInferenceServiceController();
|
|
651
|
+
const customCatalog = [{ name: 'v1', framework: 'sklearn' }, { name: 'v2', framework: 'xgboost' }];
|
|
652
|
+
const resource = makeInferenceService('multi-model', { modelCatalog: customCatalog });
|
|
653
|
+
const config = controller.toProviderConfig(resource, 'http://localhost:8080');
|
|
654
|
+
|
|
655
|
+
assert.deepEqual(config.modelCatalog, customCatalog, 'must use custom model catalog');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
// 45. KradleInferenceService resource creation via createResource
|
|
660
|
+
// ---------------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
test('KradleInferenceService can be created via createResource', () => {
|
|
663
|
+
const resource = createResource('KradleInferenceService', { name: 'test-isvc', namespace: 'kradle-org-default' }, {
|
|
664
|
+
organizationRef: 'default',
|
|
665
|
+
modelFormat: 'tensorflow',
|
|
666
|
+
storageUri: 's3://models/tf/bert'
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
assert.equal(resource.kind, 'KradleInferenceService', 'kind must match');
|
|
670
|
+
assert.equal(resource.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
|
|
671
|
+
assert.equal(resource.spec.modelFormat, 'tensorflow', 'modelFormat must be set');
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// ---------------------------------------------------------------------------
|
|
675
|
+
// 46. KradleServingRuntime resource creation via createResource
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
test('KradleServingRuntime can be created via createResource', () => {
|
|
679
|
+
const resource = createResource('KradleServingRuntime', { name: 'triton-rt', namespace: 'kradle-org-default' }, {
|
|
680
|
+
organizationRef: 'default',
|
|
681
|
+
supportedModelFormats: [{ name: 'onnx', version: '1' }],
|
|
682
|
+
containers: [{ name: 'triton', image: 'nvcr.io/nvidia/tritonserver:latest' }]
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
assert.equal(resource.kind, 'KradleServingRuntime', 'kind must match');
|
|
686
|
+
assert.equal(resource.apiVersion, 'kradle.a5c.ai/v1alpha1', 'apiVersion must match');
|
|
687
|
+
assert.ok(Array.isArray(resource.spec.supportedModelFormats), 'supportedModelFormats must be array');
|
|
688
|
+
assert.ok(Array.isArray(resource.spec.containers), 'containers must be array');
|
|
689
|
+
});
|