@a5c-ai/krate 5.0.1-staging.00fa5317c
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 +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +3205 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +3125 -0
- package/dist/krate-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-krate-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/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/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/krate-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 +1275 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -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/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 +63 -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 +93 -0
- package/scripts/validate-ui.mjs +278 -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 +209 -0
- package/src/agent-gateway-config-controller.js +147 -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 +280 -0
- package/src/agent-permission-review.js +250 -0
- package/src/agent-project-controller.js +117 -0
- package/src/agent-provider-config-controller.js +150 -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 +347 -0
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +381 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +541 -0
- package/src/argocd-gitops.js +43 -0
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +307 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +72 -0
- package/src/controller-ui.js +617 -0
- package/src/data-plane.js +179 -0
- package/src/event-bus.js +61 -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 +161 -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/hooks-events.js +63 -0
- package/src/http-server.js +377 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +57 -0
- package/src/kubernetes-controller-async.js +511 -0
- package/src/kubernetes-controller.js +878 -0
- package/src/kubernetes-resource-gateway.js +48 -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 +221 -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/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 +315 -0
- package/tests/agent-gateway-config-controller.test.js +386 -0
- package/tests/agent-memory-controller.test.js +308 -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 +204 -0
- package/tests/agent-permission-review-v2.test.js +317 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-project-controller.test.js +302 -0
- package/tests/agent-provider-config-controller.test.js +376 -0
- package/tests/agent-resources.test.js +228 -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 +221 -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 +211 -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/async-controller.test.js +252 -0
- package/tests/audit-controller.test.js +227 -0
- package/tests/codespace-controller.test.js +318 -0
- package/tests/deployment.test.js +407 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/event-bus-integration.test.js +190 -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 +340 -0
- package/tests/external-provider-adapter.test.js +365 -0
- package/tests/external-resource-model.test.js +215 -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/integration/full-flow.test.js +266 -0
- package/tests/krate.test.js +756 -0
- package/tests/memory-search-wiring.test.js +270 -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 +247 -0
- package/tests/sse-events.test.js +107 -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,327 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createRunnerController, RUNNER_CONTROLLER_BOUNDARY } from '../src/runner-controller.js';
|
|
4
|
+
|
|
5
|
+
function makePool(overrides = {}) {
|
|
6
|
+
return {
|
|
7
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
8
|
+
kind: 'RunnerPool',
|
|
9
|
+
metadata: {
|
|
10
|
+
name: overrides.name || 'test-pool',
|
|
11
|
+
namespace: overrides.namespace || 'krate-org-default',
|
|
12
|
+
labels: overrides.labels || {}
|
|
13
|
+
},
|
|
14
|
+
spec: {
|
|
15
|
+
organizationRef: 'organizationRef' in overrides ? overrides.organizationRef : 'default',
|
|
16
|
+
image: overrides.image || 'ubuntu:24.04',
|
|
17
|
+
warmReplicas: overrides.warmReplicas ?? 2,
|
|
18
|
+
maxReplicas: overrides.maxReplicas ?? 10,
|
|
19
|
+
trustTier: overrides.trustTier || 'trusted',
|
|
20
|
+
serviceAccount: overrides.serviceAccount || null,
|
|
21
|
+
resourceLimits: overrides.resourceLimits || {},
|
|
22
|
+
resourceRequests: overrides.resourceRequests || {}
|
|
23
|
+
},
|
|
24
|
+
status: { readyReplicas: overrides.readyReplicas ?? 2, queueDepth: 0 }
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makeJob(name = 'job-001') {
|
|
29
|
+
return {
|
|
30
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
31
|
+
kind: 'Job',
|
|
32
|
+
metadata: { name, namespace: 'krate-org-default' },
|
|
33
|
+
spec: { pipeline: 'pipe-1', step: 'test' }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================
|
|
38
|
+
// BOUNDARY export
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
test('RUNNER_CONTROLLER_BOUNDARY is exported and describes the controller', () => {
|
|
42
|
+
assert.equal(RUNNER_CONTROLLER_BOUNDARY.role, 'runner-controller');
|
|
43
|
+
assert.ok(RUNNER_CONTROLLER_BOUNDARY.owns.includes('pool validation'));
|
|
44
|
+
assert.ok(RUNNER_CONTROLLER_BOUNDARY.owns.includes('pod spec generation'));
|
|
45
|
+
assert.ok(RUNNER_CONTROLLER_BOUNDARY.mustNotOwn.includes('Kubernetes API calls'));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ============================================================
|
|
49
|
+
// validateRunnerPool
|
|
50
|
+
// ============================================================
|
|
51
|
+
|
|
52
|
+
test('validateRunnerPool returns valid for a well-formed RunnerPool', () => {
|
|
53
|
+
const ctrl = createRunnerController();
|
|
54
|
+
const pool = makePool();
|
|
55
|
+
const result = ctrl.validateRunnerPool(pool);
|
|
56
|
+
assert.equal(result.valid, true);
|
|
57
|
+
assert.equal(result.name, 'test-pool');
|
|
58
|
+
assert.equal(result.warmReplicas, 2);
|
|
59
|
+
assert.equal(result.maxReplicas, 10);
|
|
60
|
+
assert.equal(result.organizationRef, 'default');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('validateRunnerPool rejects missing resource', () => {
|
|
64
|
+
const ctrl = createRunnerController();
|
|
65
|
+
const result = ctrl.validateRunnerPool(null);
|
|
66
|
+
assert.equal(result.valid, false);
|
|
67
|
+
assert.equal(result.reason, 'missing-resource');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('validateRunnerPool rejects missing metadata.name', () => {
|
|
71
|
+
const ctrl = createRunnerController();
|
|
72
|
+
const pool = makePool();
|
|
73
|
+
delete pool.metadata.name;
|
|
74
|
+
const result = ctrl.validateRunnerPool(pool);
|
|
75
|
+
assert.equal(result.valid, false);
|
|
76
|
+
assert.equal(result.reason, 'missing-name');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('validateRunnerPool rejects missing organizationRef', () => {
|
|
80
|
+
const ctrl = createRunnerController();
|
|
81
|
+
const pool = makePool({ organizationRef: '' });
|
|
82
|
+
const result = ctrl.validateRunnerPool(pool);
|
|
83
|
+
assert.equal(result.valid, false);
|
|
84
|
+
assert.equal(result.reason, 'missing-org');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('validateRunnerPool rejects warmReplicas > maxReplicas', () => {
|
|
88
|
+
const ctrl = createRunnerController();
|
|
89
|
+
const pool = makePool({ warmReplicas: 15, maxReplicas: 5 });
|
|
90
|
+
const result = ctrl.validateRunnerPool(pool);
|
|
91
|
+
assert.equal(result.valid, false);
|
|
92
|
+
assert.equal(result.reason, 'replicas-conflict');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('validateRunnerPool rejects negative warmReplicas', () => {
|
|
96
|
+
const ctrl = createRunnerController();
|
|
97
|
+
const pool = makePool({ warmReplicas: -1 });
|
|
98
|
+
const result = ctrl.validateRunnerPool(pool);
|
|
99
|
+
assert.equal(result.valid, false);
|
|
100
|
+
assert.equal(result.reason, 'invalid-min-replicas');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// getPoolStatus
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
test('getPoolStatus returns Empty when no runners exist', () => {
|
|
108
|
+
const ctrl = createRunnerController();
|
|
109
|
+
const pool = makePool();
|
|
110
|
+
const status = ctrl.getPoolStatus(pool);
|
|
111
|
+
assert.equal(status.phase, 'Empty');
|
|
112
|
+
assert.equal(status.idle, 0);
|
|
113
|
+
assert.equal(status.active, 0);
|
|
114
|
+
assert.equal(status.total, 0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('getPoolStatus reflects Idle runners after createRunner', () => {
|
|
118
|
+
const ctrl = createRunnerController();
|
|
119
|
+
const pool = makePool();
|
|
120
|
+
ctrl.createRunner(pool, null);
|
|
121
|
+
ctrl.createRunner(pool, null);
|
|
122
|
+
const status = ctrl.getPoolStatus(pool);
|
|
123
|
+
assert.equal(status.idle, 2);
|
|
124
|
+
assert.equal(status.active, 0);
|
|
125
|
+
assert.equal(status.phase, 'Idle');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('getPoolStatus reflects Active runners after job assignment', () => {
|
|
129
|
+
const ctrl = createRunnerController();
|
|
130
|
+
const pool = makePool();
|
|
131
|
+
ctrl.createRunner(pool, 'run-1');
|
|
132
|
+
const status = ctrl.getPoolStatus(pool);
|
|
133
|
+
assert.equal(status.active, 1);
|
|
134
|
+
assert.equal(status.phase, 'Active');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ============================================================
|
|
138
|
+
// getCapacity
|
|
139
|
+
// ============================================================
|
|
140
|
+
|
|
141
|
+
test('getCapacity returns full capacity when pool is empty', () => {
|
|
142
|
+
const ctrl = createRunnerController();
|
|
143
|
+
const pool = makePool({ maxReplicas: 5 });
|
|
144
|
+
const cap = ctrl.getCapacity(pool);
|
|
145
|
+
assert.equal(cap.maxReplicas, 5);
|
|
146
|
+
assert.equal(cap.used, 0);
|
|
147
|
+
assert.equal(cap.available, 5);
|
|
148
|
+
assert.equal(cap.utilizationPct, 0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('getCapacity accounts for Running runners', () => {
|
|
152
|
+
const ctrl = createRunnerController();
|
|
153
|
+
const pool = makePool({ maxReplicas: 4 });
|
|
154
|
+
ctrl.createRunner(pool, 'job-a');
|
|
155
|
+
ctrl.createRunner(pool, 'job-b');
|
|
156
|
+
const cap = ctrl.getCapacity(pool);
|
|
157
|
+
assert.equal(cap.used, 2);
|
|
158
|
+
assert.equal(cap.available, 2);
|
|
159
|
+
assert.equal(cap.utilizationPct, 50);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ============================================================
|
|
163
|
+
// createRunner
|
|
164
|
+
// ============================================================
|
|
165
|
+
|
|
166
|
+
test('createRunner creates an Idle runner when no runRef given', () => {
|
|
167
|
+
const ctrl = createRunnerController();
|
|
168
|
+
const pool = makePool();
|
|
169
|
+
const result = ctrl.createRunner(pool, null);
|
|
170
|
+
assert.equal(result.error, false);
|
|
171
|
+
assert.ok(result.runnerId);
|
|
172
|
+
assert.equal(result.runner.status, 'Idle');
|
|
173
|
+
assert.equal(result.runner.runRef, null);
|
|
174
|
+
assert.equal(result.runner.poolName, 'test-pool');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('createRunner creates a Running runner when runRef given', () => {
|
|
178
|
+
const ctrl = createRunnerController();
|
|
179
|
+
const pool = makePool();
|
|
180
|
+
const result = ctrl.createRunner(pool, 'run-42');
|
|
181
|
+
assert.equal(result.error, false);
|
|
182
|
+
assert.equal(result.runner.status, 'Running');
|
|
183
|
+
assert.equal(result.runner.runRef, 'run-42');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('createRunner returns error when pool has no name', () => {
|
|
187
|
+
const ctrl = createRunnerController();
|
|
188
|
+
const result = ctrl.createRunner({ spec: {} }, null);
|
|
189
|
+
assert.equal(result.error, true);
|
|
190
|
+
assert.equal(result.reason, 'missing-pool');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ============================================================
|
|
194
|
+
// terminateRunner
|
|
195
|
+
// ============================================================
|
|
196
|
+
|
|
197
|
+
test('terminateRunner removes runner from registry', () => {
|
|
198
|
+
const ctrl = createRunnerController();
|
|
199
|
+
const pool = makePool();
|
|
200
|
+
const { runnerId } = ctrl.createRunner(pool, null);
|
|
201
|
+
const result = ctrl.terminateRunner(runnerId);
|
|
202
|
+
assert.equal(result.error, false);
|
|
203
|
+
assert.ok(result.terminatedAt);
|
|
204
|
+
assert.equal(ctrl.getRunner(runnerId), null);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('terminateRunner returns error for unknown runner', () => {
|
|
208
|
+
const ctrl = createRunnerController();
|
|
209
|
+
const result = ctrl.terminateRunner('no-such-runner');
|
|
210
|
+
assert.equal(result.error, true);
|
|
211
|
+
assert.equal(result.reason, 'not-found');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ============================================================
|
|
215
|
+
// scheduleJob
|
|
216
|
+
// ============================================================
|
|
217
|
+
|
|
218
|
+
test('scheduleJob assigns an idle runner to a job', () => {
|
|
219
|
+
const ctrl = createRunnerController();
|
|
220
|
+
const pool = makePool();
|
|
221
|
+
ctrl.createRunner(pool, null); // seed an idle runner
|
|
222
|
+
const job = makeJob('job-x');
|
|
223
|
+
const result = ctrl.scheduleJob(pool, job);
|
|
224
|
+
assert.equal(result.error, false);
|
|
225
|
+
assert.ok(result.runnerId);
|
|
226
|
+
assert.equal(result.reused, false);
|
|
227
|
+
const runner = ctrl.getRunnerForJob('job-x');
|
|
228
|
+
assert.ok(runner);
|
|
229
|
+
assert.equal(runner.status, 'Running');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('scheduleJob creates a new runner when no idle runner exists', () => {
|
|
233
|
+
const ctrl = createRunnerController();
|
|
234
|
+
const pool = makePool({ maxReplicas: 5 });
|
|
235
|
+
const job = makeJob('job-new');
|
|
236
|
+
const result = ctrl.scheduleJob(pool, job);
|
|
237
|
+
assert.equal(result.error, false);
|
|
238
|
+
assert.ok(result.runnerId);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('scheduleJob returns no-capacity error when pool is full', () => {
|
|
242
|
+
const ctrl = createRunnerController();
|
|
243
|
+
const pool = makePool({ maxReplicas: 1 });
|
|
244
|
+
ctrl.createRunner(pool, 'run-already'); // fill the pool
|
|
245
|
+
const job = makeJob('job-over-limit');
|
|
246
|
+
const result = ctrl.scheduleJob(pool, job);
|
|
247
|
+
assert.equal(result.error, true);
|
|
248
|
+
assert.equal(result.reason, 'no-capacity');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('scheduleJob is idempotent for already-assigned job', () => {
|
|
252
|
+
const ctrl = createRunnerController();
|
|
253
|
+
const pool = makePool();
|
|
254
|
+
const job = makeJob('job-dup');
|
|
255
|
+
const first = ctrl.scheduleJob(pool, job);
|
|
256
|
+
const second = ctrl.scheduleJob(pool, job);
|
|
257
|
+
assert.equal(first.runnerId, second.runnerId);
|
|
258
|
+
assert.equal(second.reused, true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ============================================================
|
|
262
|
+
// getRunnerForJob
|
|
263
|
+
// ============================================================
|
|
264
|
+
|
|
265
|
+
test('getRunnerForJob returns null for unscheduled job', () => {
|
|
266
|
+
const ctrl = createRunnerController();
|
|
267
|
+
assert.equal(ctrl.getRunnerForJob('no-job'), null);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ============================================================
|
|
271
|
+
// generatePodSpec
|
|
272
|
+
// ============================================================
|
|
273
|
+
|
|
274
|
+
test('generatePodSpec returns valid K8s Pod with workspace volume mount', () => {
|
|
275
|
+
const ctrl = createRunnerController();
|
|
276
|
+
const pool = makePool({ organizationRef: 'acme' });
|
|
277
|
+
const workspace = { runRef: 'run-99', mountPath: '/workspace', pvcName: 'krate-ws-run-99' };
|
|
278
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'runner-abc', pool }, workspace);
|
|
279
|
+
assert.equal(podSpec.kind, 'Pod');
|
|
280
|
+
assert.equal(podSpec.spec.containers[0].image, 'ubuntu:24.04');
|
|
281
|
+
assert.ok(podSpec.spec.volumes.some((v) => v.persistentVolumeClaim?.claimName === 'krate-ws-run-99'));
|
|
282
|
+
assert.ok(podSpec.spec.containers[0].volumeMounts.some((m) => m.mountPath === '/workspace'));
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('generatePodSpec injects required env vars', () => {
|
|
286
|
+
const ctrl = createRunnerController();
|
|
287
|
+
const pool = makePool({ organizationRef: 'myorg' });
|
|
288
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'runner-xyz', pool }, { runRef: 'run-7', mountPath: '/workspace', pvcName: 'krate-ws-run-7' });
|
|
289
|
+
const env = podSpec.spec.containers[0].env;
|
|
290
|
+
assert.ok(env.some((e) => e.name === 'KRATE_ORG' && e.value === 'myorg'));
|
|
291
|
+
assert.ok(env.some((e) => e.name === 'KRATE_RUN_ID'));
|
|
292
|
+
assert.ok(env.some((e) => e.name === 'KRATE_WORKSPACE_PATH'));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('generatePodSpec uses serviceAccount from pool spec', () => {
|
|
296
|
+
const ctrl = createRunnerController();
|
|
297
|
+
const pool = makePool({ serviceAccount: 'custom-sa' });
|
|
298
|
+
pool.spec.serviceAccount = 'custom-sa';
|
|
299
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'r', pool }, null);
|
|
300
|
+
assert.equal(podSpec.spec.serviceAccountName, 'custom-sa');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('generatePodSpec applies resource limits from pool config', () => {
|
|
304
|
+
const ctrl = createRunnerController();
|
|
305
|
+
const pool = makePool();
|
|
306
|
+
pool.spec.resourceLimits = { cpu: '4', memory: '8Gi' };
|
|
307
|
+
pool.spec.resourceRequests = { cpu: '1', memory: '2Gi' };
|
|
308
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'r', pool }, null);
|
|
309
|
+
assert.equal(podSpec.spec.containers[0].resources.limits.cpu, '4');
|
|
310
|
+
assert.equal(podSpec.spec.containers[0].resources.requests.memory, '2Gi');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('generatePodSpec has no volumes when workspace is null', () => {
|
|
314
|
+
const ctrl = createRunnerController();
|
|
315
|
+
const pool = makePool();
|
|
316
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'r', pool }, null);
|
|
317
|
+
assert.equal(podSpec.spec.volumes.length, 0);
|
|
318
|
+
assert.equal(podSpec.spec.containers[0].volumeMounts.length, 0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('generatePodSpec sets pool and org labels on Pod', () => {
|
|
322
|
+
const ctrl = createRunnerController();
|
|
323
|
+
const pool = makePool({ name: 'my-pool', organizationRef: 'acme' });
|
|
324
|
+
const podSpec = ctrl.generatePodSpec({ runnerId: 'r', pool }, null);
|
|
325
|
+
assert.equal(podSpec.metadata.labels['krate.a5c.ai/pool'], 'my-pool');
|
|
326
|
+
assert.equal(podSpec.metadata.labels['krate.a5c.ai/org'], 'acme');
|
|
327
|
+
});
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner controller integration tests
|
|
3
|
+
*
|
|
4
|
+
* Exercises end-to-end runner workflows: pool creation with workspace,
|
|
5
|
+
* job scheduling, pod spec generation with volume mounts, and capacity checks.
|
|
6
|
+
*/
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import test from 'node:test';
|
|
9
|
+
import { createRunnerController } from '../src/runner-controller.js';
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Fixtures
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function makePool(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
18
|
+
kind: 'RunnerPool',
|
|
19
|
+
metadata: {
|
|
20
|
+
name: overrides.name || 'integration-pool',
|
|
21
|
+
namespace: overrides.namespace || 'krate-org-default',
|
|
22
|
+
},
|
|
23
|
+
spec: {
|
|
24
|
+
organizationRef: overrides.organizationRef ?? 'default',
|
|
25
|
+
image: overrides.image || 'ubuntu:24.04',
|
|
26
|
+
warmReplicas: overrides.warmReplicas ?? 2,
|
|
27
|
+
maxReplicas: overrides.maxReplicas ?? 8,
|
|
28
|
+
trustTier: overrides.trustTier || 'trusted',
|
|
29
|
+
serviceAccount: overrides.serviceAccount || null,
|
|
30
|
+
resourceLimits: overrides.resourceLimits || {},
|
|
31
|
+
resourceRequests: overrides.resourceRequests || {},
|
|
32
|
+
},
|
|
33
|
+
status: { readyReplicas: overrides.readyReplicas ?? 2, queueDepth: 0 },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeWorkspace(runRef = 'run-ws-001') {
|
|
38
|
+
return {
|
|
39
|
+
runRef,
|
|
40
|
+
mountPath: '/workspace',
|
|
41
|
+
pvcName: `krate-ws-${runRef}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeJob(name = 'job-int-001') {
|
|
46
|
+
return {
|
|
47
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
48
|
+
kind: 'Job',
|
|
49
|
+
metadata: { name, namespace: 'krate-org-default' },
|
|
50
|
+
spec: { pipeline: 'pipe-int', step: 'build' },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// createRunner with workspace generates correct pod spec
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
test('createRunner with workspace: generatePodSpec includes workspace volume mount', () => {
|
|
59
|
+
const ctrl = createRunnerController();
|
|
60
|
+
const pool = makePool({ organizationRef: 'acme', name: 'acme-pool' });
|
|
61
|
+
const workspace = makeWorkspace('run-42');
|
|
62
|
+
|
|
63
|
+
const { runnerId, runner } = ctrl.createRunner(pool, 'run-42');
|
|
64
|
+
assert.equal(runner.status, 'Running');
|
|
65
|
+
|
|
66
|
+
const podSpec = ctrl.generatePodSpec({ runnerId, pool }, workspace);
|
|
67
|
+
|
|
68
|
+
// Volume is present
|
|
69
|
+
const vol = podSpec.spec.volumes.find(
|
|
70
|
+
(v) => v.persistentVolumeClaim?.claimName === 'krate-ws-run-42'
|
|
71
|
+
);
|
|
72
|
+
assert.ok(vol, 'pod spec must include the workspace PVC volume');
|
|
73
|
+
|
|
74
|
+
// Volume mount is present on the container
|
|
75
|
+
const mount = podSpec.spec.containers[0].volumeMounts.find(
|
|
76
|
+
(m) => m.mountPath === '/workspace'
|
|
77
|
+
);
|
|
78
|
+
assert.ok(mount, 'pod spec container must mount the workspace at /workspace');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('createRunner with workspace: pod spec has correct org label', () => {
|
|
82
|
+
const ctrl = createRunnerController();
|
|
83
|
+
const pool = makePool({ organizationRef: 'my-org', name: 'my-pool' });
|
|
84
|
+
const workspace = makeWorkspace('run-99');
|
|
85
|
+
|
|
86
|
+
const { runnerId } = ctrl.createRunner(pool, 'run-99');
|
|
87
|
+
const podSpec = ctrl.generatePodSpec({ runnerId, pool }, workspace);
|
|
88
|
+
|
|
89
|
+
assert.equal(podSpec.metadata.labels['krate.a5c.ai/org'], 'my-org');
|
|
90
|
+
assert.equal(podSpec.metadata.labels['krate.a5c.ai/pool'], 'my-pool');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// scheduleJob assigns runner to job
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
test('scheduleJob assigns an idle pre-warmed runner to a job', () => {
|
|
98
|
+
const ctrl = createRunnerController();
|
|
99
|
+
const pool = makePool({ maxReplicas: 5 });
|
|
100
|
+
|
|
101
|
+
// Pre-warm two idle runners
|
|
102
|
+
ctrl.createRunner(pool, null);
|
|
103
|
+
ctrl.createRunner(pool, null);
|
|
104
|
+
|
|
105
|
+
const job = makeJob('job-sched-001');
|
|
106
|
+
const result = ctrl.scheduleJob(pool, job);
|
|
107
|
+
|
|
108
|
+
assert.equal(result.error, false);
|
|
109
|
+
assert.ok(result.runnerId, 'scheduleJob must return a runnerId');
|
|
110
|
+
assert.equal(result.reused, false, 'first schedule should not reuse an existing assignment');
|
|
111
|
+
|
|
112
|
+
const runner = ctrl.getRunnerForJob('job-sched-001');
|
|
113
|
+
assert.ok(runner, 'runner must be retrievable by job name after scheduling');
|
|
114
|
+
assert.equal(runner.status, 'Running');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('scheduleJob is idempotent: same runnerId returned on repeat calls for same job', () => {
|
|
118
|
+
const ctrl = createRunnerController();
|
|
119
|
+
const pool = makePool();
|
|
120
|
+
const job = makeJob('job-idem-001');
|
|
121
|
+
|
|
122
|
+
const first = ctrl.scheduleJob(pool, job);
|
|
123
|
+
const second = ctrl.scheduleJob(pool, job);
|
|
124
|
+
|
|
125
|
+
assert.equal(first.runnerId, second.runnerId, 'repeated scheduleJob must return same runner');
|
|
126
|
+
assert.equal(second.reused, true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('scheduleJob returns no-capacity when pool is exhausted', () => {
|
|
130
|
+
const ctrl = createRunnerController();
|
|
131
|
+
const pool = makePool({ maxReplicas: 2 });
|
|
132
|
+
|
|
133
|
+
ctrl.createRunner(pool, 'run-a');
|
|
134
|
+
ctrl.createRunner(pool, 'run-b');
|
|
135
|
+
|
|
136
|
+
const job = makeJob('job-over-cap');
|
|
137
|
+
const result = ctrl.scheduleJob(pool, job);
|
|
138
|
+
|
|
139
|
+
assert.equal(result.error, true);
|
|
140
|
+
assert.equal(result.reason, 'no-capacity');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// generatePodSpec includes workspace volume mount
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
test('generatePodSpec with workspace sets KRATE_WORKSPACE_PATH env var', () => {
|
|
148
|
+
const ctrl = createRunnerController();
|
|
149
|
+
const pool = makePool({ organizationRef: 'corp' });
|
|
150
|
+
const workspace = makeWorkspace('run-77');
|
|
151
|
+
const { runnerId } = ctrl.createRunner(pool, 'run-77');
|
|
152
|
+
|
|
153
|
+
const podSpec = ctrl.generatePodSpec({ runnerId, pool }, workspace);
|
|
154
|
+
const env = podSpec.spec.containers[0].env;
|
|
155
|
+
|
|
156
|
+
const wsPathEnv = env.find((e) => e.name === 'KRATE_WORKSPACE_PATH');
|
|
157
|
+
assert.ok(wsPathEnv, 'KRATE_WORKSPACE_PATH env var must be present');
|
|
158
|
+
assert.equal(wsPathEnv.value, '/workspace');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('generatePodSpec without workspace has empty volumes and volumeMounts', () => {
|
|
162
|
+
const ctrl = createRunnerController();
|
|
163
|
+
const pool = makePool({ organizationRef: 'corp' });
|
|
164
|
+
const { runnerId } = ctrl.createRunner(pool, null);
|
|
165
|
+
|
|
166
|
+
const podSpec = ctrl.generatePodSpec({ runnerId, pool }, null);
|
|
167
|
+
|
|
168
|
+
assert.equal(podSpec.spec.volumes.length, 0);
|
|
169
|
+
assert.equal(podSpec.spec.containers[0].volumeMounts.length, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// getCapacity returns correct available slots
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
test('getCapacity returns correct available slots after active runners are created', () => {
|
|
177
|
+
const ctrl = createRunnerController();
|
|
178
|
+
const pool = makePool({ maxReplicas: 6 });
|
|
179
|
+
|
|
180
|
+
ctrl.createRunner(pool, 'run-c1');
|
|
181
|
+
ctrl.createRunner(pool, 'run-c2');
|
|
182
|
+
// Only Running (active) runners count toward utilization; Idle runners do not
|
|
183
|
+
ctrl.createRunner(pool, null); // idle runner does NOT count as used
|
|
184
|
+
|
|
185
|
+
const cap = ctrl.getCapacity(pool);
|
|
186
|
+
assert.equal(cap.maxReplicas, 6);
|
|
187
|
+
assert.equal(cap.used, 2); // only the two Running runners
|
|
188
|
+
assert.equal(cap.available, 4);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('getCapacity shows 0% utilization on empty pool', () => {
|
|
192
|
+
const ctrl = createRunnerController();
|
|
193
|
+
const pool = makePool({ maxReplicas: 10 });
|
|
194
|
+
const cap = ctrl.getCapacity(pool);
|
|
195
|
+
|
|
196
|
+
assert.equal(cap.used, 0);
|
|
197
|
+
assert.equal(cap.available, 10);
|
|
198
|
+
assert.equal(cap.utilizationPct, 0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('getCapacity shows 100% utilization when pool is full', () => {
|
|
202
|
+
const ctrl = createRunnerController();
|
|
203
|
+
const pool = makePool({ maxReplicas: 3 });
|
|
204
|
+
ctrl.createRunner(pool, 'run-f1');
|
|
205
|
+
ctrl.createRunner(pool, 'run-f2');
|
|
206
|
+
ctrl.createRunner(pool, 'run-f3');
|
|
207
|
+
|
|
208
|
+
const cap = ctrl.getCapacity(pool);
|
|
209
|
+
assert.equal(cap.available, 0);
|
|
210
|
+
assert.equal(cap.utilizationPct, 100);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Full workflow: pool → schedule → terminate → verify capacity restored
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
test('terminating a runner restores pool capacity', () => {
|
|
218
|
+
const ctrl = createRunnerController();
|
|
219
|
+
const pool = makePool({ maxReplicas: 2 });
|
|
220
|
+
|
|
221
|
+
const { runnerId } = ctrl.createRunner(pool, 'run-term-1');
|
|
222
|
+
ctrl.createRunner(pool, 'run-term-2');
|
|
223
|
+
|
|
224
|
+
const beforeTerminate = ctrl.getCapacity(pool);
|
|
225
|
+
assert.equal(beforeTerminate.available, 0);
|
|
226
|
+
|
|
227
|
+
ctrl.terminateRunner(runnerId);
|
|
228
|
+
|
|
229
|
+
const afterTerminate = ctrl.getCapacity(pool);
|
|
230
|
+
assert.equal(afterTerminate.available, 1);
|
|
231
|
+
});
|