@a5c-ai/krate 5.0.1-staging.660d2b90f → 5.0.1-staging.69cb593ea
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 -29
- package/bin/krate-demo.mjs +0 -0
- package/bin/krate-server.mjs +0 -0
- package/dist/krate-controller-ui.json +808 -10
- package/dist/krate-lifecycle.json +1 -1
- package/dist/krate-runtime-snapshot.json +223 -53
- package/dist/krate-summary.json +40 -3
- package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
- package/docs/architecture-v2.md +431 -0
- package/docs/openapi.yaml +1275 -0
- package/docs/requirements-v2.md +238 -0
- package/docs/sdk-api-reference.md +782 -0
- package/docs/system-spec-v2.md +352 -0
- package/docs/todos.md +4 -0
- package/docs/web-console-spec.md +433 -0
- package/package.json +1 -1
- package/scripts/validate-ui.mjs +305 -207
- package/src/agent-adapter-controller.js +169 -0
- package/src/agent-approval-controller.js +47 -0
- package/src/agent-dispatch-controller.js +130 -7
- 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 +1 -1
- package/src/agent-permission-review.js +102 -14
- 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 +52 -1
- package/src/agent-subagent-controller.js +160 -0
- package/src/agent-transport-binding-controller.js +121 -0
- package/src/agent-trigger-controller.js +273 -0
- package/src/agent-workspace-controller.js +702 -0
- package/src/agent-writeback-controller.js +302 -0
- package/src/api-controller.js +338 -3
- package/src/async-controller.js +207 -0
- package/src/audit-controller.js +191 -0
- package/src/auth.js +48 -6
- package/src/controller-client.js +112 -38
- package/src/controller-ui.js +96 -16
- package/src/data-plane.js +3 -2
- 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 +36 -0
- package/src/gitea-service.js +173 -0
- package/src/http-server.js +226 -0
- package/src/index.js +27 -0
- package/src/kubernetes-controller-async.js +531 -0
- package/src/kubernetes-controller.js +156 -84
- package/src/notification-controller.js +178 -0
- package/src/org-scoping.js +5 -0
- package/src/resource-model.js +26 -8
- package/src/runner-controller.js +272 -0
- package/src/snapshot-cache.js +157 -0
- package/tests/agent-adapter-controller.test.js +361 -0
- package/tests/agent-dispatch-controller.test.js +139 -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-permission-review-v2.test.js +317 -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 +35 -19
- package/tests/agent-secret-config-grant.test.js +231 -0
- package/tests/agent-session-transcript-controller.test.js +499 -0
- package/tests/agent-subagent-controller.test.js +201 -0
- package/tests/agent-transport-binding-controller.test.js +294 -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/controller-client.test.js +133 -0
- package/tests/deployment.test.js +43 -29
- package/tests/e2e/lifecycle.test.js +5 -2
- 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 +58 -6
- 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 +315 -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,318 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentWorkspaceController, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function makeWorkspace(name, repository, phase = 'Ready', extra = {}) {
|
|
6
|
+
const workspace = createResource('KrateWorkspace', { name, namespace: 'krate-org-default' }, {
|
|
7
|
+
organizationRef: extra.organizationRef || 'default',
|
|
8
|
+
repository,
|
|
9
|
+
volumeSpec: extra.volumeSpec || { storageClassName: 'standard', capacity: '10Gi', accessModes: ['ReadWriteOnce'] },
|
|
10
|
+
branch: extra.branch || 'main',
|
|
11
|
+
pvcName: extra.pvcName || `krate-ws-${name}`,
|
|
12
|
+
});
|
|
13
|
+
workspace.status = { phase, createdAt: new Date().toISOString(), volumeStatus: 'Bound', ...extra.status };
|
|
14
|
+
if (extra.associations) {
|
|
15
|
+
workspace.spec.associations = extra.associations;
|
|
16
|
+
}
|
|
17
|
+
return workspace;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeRun(name, workspaceRef, phase = 'Succeeded', extra = {}) {
|
|
21
|
+
const run = createResource('AgentDispatchRun', { name, namespace: 'krate-org-default' }, {
|
|
22
|
+
organizationRef: 'default',
|
|
23
|
+
repository: extra.repository || 'my-repo',
|
|
24
|
+
sourceRefs: extra.sourceRefs || ['main'],
|
|
25
|
+
agentStack: extra.agentStack || 'code-agent',
|
|
26
|
+
taskKind: extra.taskKind || 'diagnostic',
|
|
27
|
+
workspaceRef,
|
|
28
|
+
});
|
|
29
|
+
run.status = {
|
|
30
|
+
phase,
|
|
31
|
+
workspaceRef,
|
|
32
|
+
createdAt: extra.createdAt || new Date().toISOString(),
|
|
33
|
+
...extra.status,
|
|
34
|
+
};
|
|
35
|
+
return run;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- launchCodespace ---
|
|
39
|
+
|
|
40
|
+
test('launchCodespace returns valid pod spec with PVC mount', () => {
|
|
41
|
+
const controller = createAgentWorkspaceController();
|
|
42
|
+
const ws = makeWorkspace('ws-dev', 'my-repo');
|
|
43
|
+
const result = controller.launchCodespace(ws);
|
|
44
|
+
|
|
45
|
+
assert.equal(result.error, false);
|
|
46
|
+
assert.ok(result.podSpec, 'Should return podSpec');
|
|
47
|
+
assert.equal(result.podSpec.kind, 'Pod');
|
|
48
|
+
assert.equal(result.podSpec.metadata.name, 'codespace-ws-dev');
|
|
49
|
+
|
|
50
|
+
// Verify PVC mount
|
|
51
|
+
const volumes = result.podSpec.spec.volumes;
|
|
52
|
+
assert.ok(volumes.some((v) => v.persistentVolumeClaim?.claimName === 'krate-ws-ws-dev'), 'PVC should be mounted');
|
|
53
|
+
const container = result.podSpec.spec.containers[0];
|
|
54
|
+
assert.ok(container.volumeMounts.some((m) => m.mountPath === '/workspace'), 'Should mount at /workspace');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('launchCodespace uses custom image when provided', () => {
|
|
58
|
+
const controller = createAgentWorkspaceController();
|
|
59
|
+
const ws = makeWorkspace('ws-custom', 'my-repo');
|
|
60
|
+
const result = controller.launchCodespace(ws, { image: 'gitpod/openvscode-server:latest' });
|
|
61
|
+
|
|
62
|
+
assert.equal(result.error, false);
|
|
63
|
+
assert.equal(result.podSpec.spec.containers[0].image, 'gitpod/openvscode-server:latest');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('launchCodespace rejects if codespace already running', () => {
|
|
67
|
+
const controller = createAgentWorkspaceController();
|
|
68
|
+
const ws = makeWorkspace('ws-running', 'my-repo', 'Ready', {
|
|
69
|
+
status: { codespace: { running: true } },
|
|
70
|
+
});
|
|
71
|
+
const result = controller.launchCodespace(ws);
|
|
72
|
+
|
|
73
|
+
assert.equal(result.error, true);
|
|
74
|
+
assert.equal(result.reason, 'codespace-already-running');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('launchCodespace pod spec has correct resource limits', () => {
|
|
78
|
+
const controller = createAgentWorkspaceController();
|
|
79
|
+
const ws = makeWorkspace('ws-limits', 'my-repo');
|
|
80
|
+
const result = controller.launchCodespace(ws, { cpu: '2', memory: '4Gi' });
|
|
81
|
+
|
|
82
|
+
assert.equal(result.error, false);
|
|
83
|
+
const container = result.podSpec.spec.containers[0];
|
|
84
|
+
assert.equal(container.resources.limits.cpu, '2');
|
|
85
|
+
assert.equal(container.resources.limits.memory, '4Gi');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('launchCodespace pod spec has correct env vars', () => {
|
|
89
|
+
const controller = createAgentWorkspaceController();
|
|
90
|
+
const ws = makeWorkspace('ws-env', 'my-repo');
|
|
91
|
+
const result = controller.launchCodespace(ws, { gitAuthorName: 'Test User' });
|
|
92
|
+
|
|
93
|
+
assert.equal(result.error, false);
|
|
94
|
+
const container = result.podSpec.spec.containers[0];
|
|
95
|
+
const envMap = {};
|
|
96
|
+
for (const e of container.env) envMap[e.name] = e.value || e.valueFrom;
|
|
97
|
+
|
|
98
|
+
assert.equal(envMap.KRATE_WORKSPACE, 'ws-env');
|
|
99
|
+
assert.equal(envMap.KRATE_ORG, 'default');
|
|
100
|
+
assert.equal(envMap.GIT_AUTHOR_NAME, 'Test User');
|
|
101
|
+
assert.ok(envMap.GIT_AUTHOR_EMAIL, 'Should have GIT_AUTHOR_EMAIL');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('launchCodespace service spec exposes port 8080', () => {
|
|
105
|
+
const controller = createAgentWorkspaceController();
|
|
106
|
+
const ws = makeWorkspace('ws-svc', 'my-repo');
|
|
107
|
+
const result = controller.launchCodespace(ws);
|
|
108
|
+
|
|
109
|
+
assert.equal(result.error, false);
|
|
110
|
+
assert.ok(result.serviceSpec, 'Should return serviceSpec');
|
|
111
|
+
assert.equal(result.serviceSpec.kind, 'Service');
|
|
112
|
+
const ports = result.serviceSpec.spec.ports;
|
|
113
|
+
assert.ok(ports.some((p) => p.port === 8080), 'Service should expose port 8080');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('launchCodespace returns codespace URL', () => {
|
|
117
|
+
const controller = createAgentWorkspaceController();
|
|
118
|
+
const ws = makeWorkspace('ws-url', 'my-repo');
|
|
119
|
+
const result = controller.launchCodespace(ws);
|
|
120
|
+
|
|
121
|
+
assert.equal(result.error, false);
|
|
122
|
+
assert.ok(result.codespaceUrl, 'Should return codespaceUrl');
|
|
123
|
+
assert.ok(result.codespaceUrl.includes('codespace-svc-ws-url'), 'URL should reference service name');
|
|
124
|
+
assert.ok(result.codespaceUrl.includes('8080'), 'URL should include port');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('launchCodespace includes password secret ref when provided', () => {
|
|
128
|
+
const controller = createAgentWorkspaceController();
|
|
129
|
+
const ws = makeWorkspace('ws-pw', 'my-repo');
|
|
130
|
+
const secretRef = { name: 'my-secret', key: 'password' };
|
|
131
|
+
const result = controller.launchCodespace(ws, { passwordSecretRef: secretRef });
|
|
132
|
+
|
|
133
|
+
assert.equal(result.error, false);
|
|
134
|
+
const container = result.podSpec.spec.containers[0];
|
|
135
|
+
const pwEnv = container.env.find((e) => e.name === 'PASSWORD');
|
|
136
|
+
assert.ok(pwEnv, 'Should have PASSWORD env');
|
|
137
|
+
assert.deepEqual(pwEnv.valueFrom.secretKeyRef, secretRef);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('launchCodespace returns error when workspace is null', () => {
|
|
141
|
+
const controller = createAgentWorkspaceController();
|
|
142
|
+
const result = controller.launchCodespace(null);
|
|
143
|
+
|
|
144
|
+
assert.equal(result.error, true);
|
|
145
|
+
assert.equal(result.reason, 'missing-workspace');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- stopCodespace ---
|
|
149
|
+
|
|
150
|
+
test('stopCodespace returns delete manifests for pod and service', () => {
|
|
151
|
+
const controller = createAgentWorkspaceController();
|
|
152
|
+
const ws = makeWorkspace('ws-stop', 'my-repo');
|
|
153
|
+
const result = controller.stopCodespace(ws);
|
|
154
|
+
|
|
155
|
+
assert.equal(result.error, false);
|
|
156
|
+
assert.ok(result.podDeleteManifest, 'Should return pod delete manifest');
|
|
157
|
+
assert.equal(result.podDeleteManifest.kind, 'Pod');
|
|
158
|
+
assert.equal(result.podDeleteManifest.metadata.name, 'codespace-ws-stop');
|
|
159
|
+
assert.equal(result.podDeleteManifest.action, 'delete');
|
|
160
|
+
|
|
161
|
+
assert.ok(result.serviceDeleteManifest, 'Should return service delete manifest');
|
|
162
|
+
assert.equal(result.serviceDeleteManifest.kind, 'Service');
|
|
163
|
+
assert.equal(result.serviceDeleteManifest.metadata.name, 'codespace-svc-ws-stop');
|
|
164
|
+
assert.equal(result.serviceDeleteManifest.action, 'delete');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// --- getCodespaceStatus ---
|
|
168
|
+
|
|
169
|
+
test('getCodespaceStatus returns running state when pod is Running', () => {
|
|
170
|
+
const controller = createAgentWorkspaceController();
|
|
171
|
+
const ws = makeWorkspace('ws-status', 'my-repo');
|
|
172
|
+
const result = controller.getCodespaceStatus(ws, { phase: 'Running', startTime: '2026-05-13T10:00:00Z', connectedUsers: 2 });
|
|
173
|
+
|
|
174
|
+
assert.equal(result.error, false);
|
|
175
|
+
assert.equal(result.running, true);
|
|
176
|
+
assert.ok(result.url, 'Should have URL when running');
|
|
177
|
+
assert.equal(result.port, 8080);
|
|
178
|
+
assert.equal(result.connectedUsers, 2);
|
|
179
|
+
assert.equal(result.startTime, '2026-05-13T10:00:00Z');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('getCodespaceStatus returns not running when no pod status', () => {
|
|
183
|
+
const controller = createAgentWorkspaceController();
|
|
184
|
+
const ws = makeWorkspace('ws-status-off', 'my-repo');
|
|
185
|
+
const result = controller.getCodespaceStatus(ws);
|
|
186
|
+
|
|
187
|
+
assert.equal(result.error, false);
|
|
188
|
+
assert.equal(result.running, false);
|
|
189
|
+
assert.equal(result.url, null);
|
|
190
|
+
assert.equal(result.phase, 'Unknown');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- addAssociation ---
|
|
194
|
+
|
|
195
|
+
test('addAssociation adds to associations array', () => {
|
|
196
|
+
const controller = createAgentWorkspaceController();
|
|
197
|
+
const ws = makeWorkspace('ws-assoc', 'my-repo');
|
|
198
|
+
const result = controller.addAssociation(ws, { kind: 'User', name: 'alice' });
|
|
199
|
+
|
|
200
|
+
assert.equal(result.error, false);
|
|
201
|
+
assert.equal(result.workspace.spec.associations.length, 1);
|
|
202
|
+
assert.equal(result.workspace.spec.associations[0].kind, 'User');
|
|
203
|
+
assert.equal(result.workspace.spec.associations[0].name, 'alice');
|
|
204
|
+
assert.ok(result.workspace.spec.associations[0].addedAt, 'Should have addedAt timestamp');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('addAssociation rejects duplicate', () => {
|
|
208
|
+
const controller = createAgentWorkspaceController();
|
|
209
|
+
const ws = makeWorkspace('ws-dup', 'my-repo', 'Ready', {
|
|
210
|
+
associations: [{ kind: 'User', name: 'alice', addedAt: new Date().toISOString() }],
|
|
211
|
+
});
|
|
212
|
+
const result = controller.addAssociation(ws, { kind: 'User', name: 'alice' });
|
|
213
|
+
|
|
214
|
+
assert.equal(result.error, true);
|
|
215
|
+
assert.equal(result.reason, 'duplicate-association');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('addAssociation rejects invalid kind', () => {
|
|
219
|
+
const controller = createAgentWorkspaceController();
|
|
220
|
+
const ws = makeWorkspace('ws-bad-kind', 'my-repo');
|
|
221
|
+
const result = controller.addAssociation(ws, { kind: 'InvalidKind', name: 'foo' });
|
|
222
|
+
|
|
223
|
+
assert.equal(result.error, true);
|
|
224
|
+
assert.equal(result.reason, 'invalid-ref-kind');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- removeAssociation ---
|
|
228
|
+
|
|
229
|
+
test('removeAssociation removes from array', () => {
|
|
230
|
+
const controller = createAgentWorkspaceController();
|
|
231
|
+
const ws = makeWorkspace('ws-rm', 'my-repo', 'Ready', {
|
|
232
|
+
associations: [
|
|
233
|
+
{ kind: 'User', name: 'alice', addedAt: new Date().toISOString() },
|
|
234
|
+
{ kind: 'AgentSession', name: 'sess-1', addedAt: new Date().toISOString() },
|
|
235
|
+
],
|
|
236
|
+
});
|
|
237
|
+
const result = controller.removeAssociation(ws, { kind: 'User', name: 'alice' });
|
|
238
|
+
|
|
239
|
+
assert.equal(result.error, false);
|
|
240
|
+
assert.equal(result.workspace.spec.associations.length, 1);
|
|
241
|
+
assert.equal(result.workspace.spec.associations[0].kind, 'AgentSession');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('removeAssociation returns error if not found', () => {
|
|
245
|
+
const controller = createAgentWorkspaceController();
|
|
246
|
+
const ws = makeWorkspace('ws-rm-nf', 'my-repo');
|
|
247
|
+
const result = controller.removeAssociation(ws, { kind: 'User', name: 'nobody' });
|
|
248
|
+
|
|
249
|
+
assert.equal(result.error, true);
|
|
250
|
+
assert.equal(result.reason, 'not-found');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- listAssociations ---
|
|
254
|
+
|
|
255
|
+
test('listAssociations returns all with kind/name', () => {
|
|
256
|
+
const controller = createAgentWorkspaceController();
|
|
257
|
+
const ws = makeWorkspace('ws-list', 'my-repo', 'Ready', {
|
|
258
|
+
associations: [
|
|
259
|
+
{ kind: 'User', name: 'alice', addedAt: '2026-01-01T00:00:00Z' },
|
|
260
|
+
{ kind: 'AgentDispatchRun', name: 'run-1', addedAt: '2026-01-02T00:00:00Z' },
|
|
261
|
+
{ kind: 'AgentSession', name: 'sess-1', addedAt: '2026-01-03T00:00:00Z' },
|
|
262
|
+
],
|
|
263
|
+
});
|
|
264
|
+
const result = controller.listAssociations(ws);
|
|
265
|
+
|
|
266
|
+
assert.equal(result.error, false);
|
|
267
|
+
assert.equal(result.associations.length, 3);
|
|
268
|
+
assert.equal(result.associations[0].kind, 'User');
|
|
269
|
+
assert.equal(result.associations[1].kind, 'AgentDispatchRun');
|
|
270
|
+
assert.equal(result.associations[2].kind, 'AgentSession');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// --- getWorkspaceRuns ---
|
|
274
|
+
|
|
275
|
+
test('getWorkspaceRuns filters active vs history', () => {
|
|
276
|
+
const controller = createAgentWorkspaceController();
|
|
277
|
+
const ws = makeWorkspace('ws-runs', 'my-repo');
|
|
278
|
+
|
|
279
|
+
const activeRun = makeRun('run-active', 'ws-runs', 'Running');
|
|
280
|
+
const doneRun = makeRun('run-done', 'ws-runs', 'Succeeded');
|
|
281
|
+
const failedRun = makeRun('run-failed', 'ws-runs', 'Failed');
|
|
282
|
+
const unrelatedRun = makeRun('run-other', 'ws-other', 'Running');
|
|
283
|
+
|
|
284
|
+
const result = controller.getWorkspaceRuns(ws, [activeRun, doneRun, failedRun, unrelatedRun]);
|
|
285
|
+
|
|
286
|
+
assert.equal(result.error, false);
|
|
287
|
+
assert.equal(result.active.length, 1, 'Should have one active run');
|
|
288
|
+
assert.equal(result.active[0].metadata.name, 'run-active');
|
|
289
|
+
assert.equal(result.history.length, 2, 'Should have two history runs');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('getWorkspaceRuns finds runs by association', () => {
|
|
293
|
+
const controller = createAgentWorkspaceController();
|
|
294
|
+
const ws = makeWorkspace('ws-assoc-runs', 'my-repo', 'Ready', {
|
|
295
|
+
associations: [{ kind: 'AgentDispatchRun', name: 'run-linked', addedAt: new Date().toISOString() }],
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// This run has no workspaceRef but is linked via association
|
|
299
|
+
const linkedRun = makeRun('run-linked', null, 'Succeeded');
|
|
300
|
+
delete linkedRun.status.workspaceRef;
|
|
301
|
+
delete linkedRun.spec.workspaceRef;
|
|
302
|
+
|
|
303
|
+
const result = controller.getWorkspaceRuns(ws, [linkedRun]);
|
|
304
|
+
|
|
305
|
+
assert.equal(result.error, false);
|
|
306
|
+
assert.equal(result.history.length, 1, 'Should find run via association');
|
|
307
|
+
assert.equal(result.history[0].metadata.name, 'run-linked');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('getWorkspaceRuns returns empty when no matching runs', () => {
|
|
311
|
+
const controller = createAgentWorkspaceController();
|
|
312
|
+
const ws = makeWorkspace('ws-empty', 'my-repo');
|
|
313
|
+
const result = controller.getWorkspaceRuns(ws, []);
|
|
314
|
+
|
|
315
|
+
assert.equal(result.error, false);
|
|
316
|
+
assert.equal(result.active.length, 0);
|
|
317
|
+
assert.equal(result.history.length, 0);
|
|
318
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { fetchControllerUiModel, createControllerUiModel, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function degradedRemoteModel() {
|
|
6
|
+
return createControllerUiModel({
|
|
7
|
+
source: 'controller-api',
|
|
8
|
+
namespace: 'krate-staging',
|
|
9
|
+
kubectl: { available: false, context: null, errors: ['fetch failed'] },
|
|
10
|
+
resources: {},
|
|
11
|
+
crds: [],
|
|
12
|
+
events: [],
|
|
13
|
+
permissions: [],
|
|
14
|
+
storage: {},
|
|
15
|
+
commands: []
|
|
16
|
+
}, { organization: 'default' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function liveSnapshot() {
|
|
20
|
+
return {
|
|
21
|
+
source: 'kubernetes',
|
|
22
|
+
namespace: 'krate-staging',
|
|
23
|
+
kubectl: { available: true, context: 'aks-krate-staging', errors: [] },
|
|
24
|
+
apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
|
|
25
|
+
crds: [{ metadata: { name: 'repositories.krate.a5c.ai' } }],
|
|
26
|
+
resources: {
|
|
27
|
+
Organization: [createResource('Organization', { name: 'default', namespace: 'krate-system' }, { slug: 'default', namespaceName: 'krate-org-default', displayName: 'Default org' })],
|
|
28
|
+
Repository: [createResource('Repository', { name: 'test2', namespace: 'krate-org-default' }, { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' })]
|
|
29
|
+
},
|
|
30
|
+
events: [],
|
|
31
|
+
permissions: [],
|
|
32
|
+
storage: {},
|
|
33
|
+
commands: []
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test('fetchControllerUiModel falls back to local snapshot when remote controller returns degraded empty data', async () => {
|
|
38
|
+
const calls = [];
|
|
39
|
+
const model = await fetchControllerUiModel({
|
|
40
|
+
controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
|
|
41
|
+
organization: 'default',
|
|
42
|
+
useCache: false,
|
|
43
|
+
fetchImpl: async () => ({ ok: true, json: async () => degradedRemoteModel() }),
|
|
44
|
+
controller: { async snapshot() { calls.push('snapshot'); return liveSnapshot(); } }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(calls, ['snapshot']);
|
|
48
|
+
assert.equal(model.status, 'ready');
|
|
49
|
+
assert.equal(model.metrics.repositories, 1);
|
|
50
|
+
assert.equal(model.views.dashboard.repositories[0].metadata.name, 'test2');
|
|
51
|
+
assert.ok(model.controller.connection.errors.length > 0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('fetchControllerUiModel falls back to local snapshot when remote controller fetch throws', async () => {
|
|
55
|
+
const model = await fetchControllerUiModel({
|
|
56
|
+
controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
|
|
57
|
+
organization: 'default',
|
|
58
|
+
useCache: false,
|
|
59
|
+
fetchImpl: async () => { throw new Error('connect ECONNREFUSED'); },
|
|
60
|
+
controller: { async snapshot() { return liveSnapshot(); } }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.equal(model.status, 'ready');
|
|
64
|
+
assert.equal(model.metrics.repositories, 1);
|
|
65
|
+
assert.match(model.controller.connection.errors[0], /ECONNREFUSED/);
|
|
66
|
+
});
|
|
67
|
+
test('fetchControllerUiModel uses bounded async fallback when remote controller hangs', async () => {
|
|
68
|
+
const calls = [];
|
|
69
|
+
const model = await fetchControllerUiModel({
|
|
70
|
+
controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
|
|
71
|
+
organization: 'default',
|
|
72
|
+
requestTimeoutMs: 10,
|
|
73
|
+
useCache: false,
|
|
74
|
+
fetchImpl: async (_target, options = {}) => new Promise((_resolve, reject) => {
|
|
75
|
+
const timer = setTimeout(() => reject(new Error('remote controller hung')), 25);
|
|
76
|
+
options.signal?.addEventListener('abort', () => {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
reject(new Error('remote controller hung'));
|
|
79
|
+
}, { once: true });
|
|
80
|
+
}),
|
|
81
|
+
fallbackSnapshot: async () => { calls.push('fallbackSnapshot'); return liveSnapshot(); }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.deepEqual(calls, ['fallbackSnapshot']);
|
|
85
|
+
assert.equal(model.status, 'ready');
|
|
86
|
+
assert.equal(model.metrics.repositories, 1);
|
|
87
|
+
assert.match(model.controller.connection.errors.join('; '), /remote controller hung/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('fetchControllerUiModel uses async fallback for degraded empty remote data without constructing sync controller', async () => {
|
|
91
|
+
const calls = [];
|
|
92
|
+
const model = await fetchControllerUiModel({
|
|
93
|
+
controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
|
|
94
|
+
organization: 'default',
|
|
95
|
+
useCache: false,
|
|
96
|
+
fetchImpl: async () => ({ ok: true, json: async () => degradedRemoteModel() }),
|
|
97
|
+
fallbackSnapshot: async () => { calls.push('fallbackSnapshot'); return liveSnapshot(); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(calls, ['fallbackSnapshot']);
|
|
101
|
+
assert.equal(model.status, 'ready');
|
|
102
|
+
assert.equal(model.views.dashboard.repositories[0].metadata.name, 'test2');
|
|
103
|
+
assert.ok(model.controller.connection.errors.length > 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
test('fetchControllerUiModel probes local snapshot when remote controller is ready but missing CRD-backed resources', async () => {
|
|
108
|
+
const calls = [];
|
|
109
|
+
const emptyRemote = createControllerUiModel({
|
|
110
|
+
source: 'kubernetes',
|
|
111
|
+
namespace: 'krate-staging',
|
|
112
|
+
kubectl: { available: true, context: 'aks-krate-staging', errors: [] },
|
|
113
|
+
apiService: { metadata: { name: 'v1alpha1.krate.a5c.ai' } },
|
|
114
|
+
resources: {},
|
|
115
|
+
crds: [],
|
|
116
|
+
events: [],
|
|
117
|
+
permissions: [],
|
|
118
|
+
storage: {},
|
|
119
|
+
commands: []
|
|
120
|
+
}, { organization: 'default' });
|
|
121
|
+
|
|
122
|
+
const model = await fetchControllerUiModel({
|
|
123
|
+
controllerUrl: 'http://krate-api.krate-staging.svc.cluster.local',
|
|
124
|
+
organization: 'default',
|
|
125
|
+
useCache: false,
|
|
126
|
+
fetchImpl: async () => ({ ok: true, json: async () => emptyRemote }),
|
|
127
|
+
fallbackSnapshot: async () => { calls.push('fallbackSnapshot'); return liveSnapshot(); }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
assert.deepEqual(calls, ['fallbackSnapshot']);
|
|
131
|
+
assert.equal(model.metrics.repositories, 1);
|
|
132
|
+
assert.equal(model.views.dashboard.repositories[0].metadata.name, 'test2');
|
|
133
|
+
});
|
package/tests/deployment.test.js
CHANGED
|
@@ -75,13 +75,11 @@ test('controller deployment assets build and publish the runnable controller', (
|
|
|
75
75
|
for (const file of ['Dockerfile', '.dockerignore', '.github/workflows/publish.yml']) assert.equal(existsSync(file), true, `${file} exists`);
|
|
76
76
|
|
|
77
77
|
const dockerfile = read('Dockerfile');
|
|
78
|
-
assert.match(dockerfile, /FROM node
|
|
79
|
-
assert.match(dockerfile, /npm ci/);
|
|
80
|
-
assert.match(dockerfile, /npm run build/);
|
|
78
|
+
assert.match(dockerfile, /FROM node:\d+-alpine AS (deps|build)/);
|
|
79
|
+
assert.match(dockerfile, /npm (ci|install)/);
|
|
81
80
|
assert.match(dockerfile, /HEALTHCHECK/);
|
|
82
|
-
assert.match(dockerfile, /
|
|
83
|
-
assert.ok(dockerfile.includes(
|
|
84
|
-
for (const runtimePath of ['/app/bin', '/app/src', '/app/dist', '/app/charts', '/app/examples']) assert.ok(dockerfile.includes(runtimePath), `Dockerfile copies ${runtimePath}`);
|
|
81
|
+
assert.match(dockerfile, /krate-server\.mjs/);
|
|
82
|
+
for (const runtimePath of ['/app/bin', '/app/src', '/app/dist']) assert.ok(dockerfile.includes(runtimePath), `Dockerfile copies ${runtimePath}`);
|
|
85
83
|
|
|
86
84
|
const dockerignore = read('.dockerignore');
|
|
87
85
|
for (const ignored of ['.a5c', 'node_modules', '**/.next', 'dist']) assert.ok(dockerignore.includes(ignored), `.dockerignore excludes ${ignored}`);
|
|
@@ -94,13 +92,14 @@ test('controller deployment assets build and publish the runnable controller', (
|
|
|
94
92
|
assert.ok(chartValues.includes('auth:'), 'chart values include auth configuration');
|
|
95
93
|
assert.ok(chartValues.includes('github:') && chartValues.includes('sso:'), 'chart values expose GitHub and SSO configuration');
|
|
96
94
|
assert.ok(chartValues.includes('mode: auto') && chartValues.includes('policyReporter:'), 'chart values expose Kyverno auto-discovery modes and policy reporter settings');
|
|
97
|
-
assert.ok(chartRbac.includes('
|
|
95
|
+
assert.ok(chartRbac.includes('"*"') && chartRbac.includes('policies.kyverno.io') && chartRbac.includes('policyreports'), 'RBAC covers all Krate resources via wildcard and Kyverno read/write surfaces');
|
|
98
96
|
assert.ok(chartDeployment.includes('KRATE_KYVERNO_MODE') && chartDeployment.includes('KRATE_KYVERNO_POLICY_NAMESPACE') && chartDeployment.includes('KRATE_KYVERNO_DISCOVER_EXISTING'), 'deployments receive Kyverno discovery env');
|
|
99
97
|
assert.ok(read('../charts/templates/argocd-kyverno-application.yaml').includes('krate.a5c.ai/policy-engine: kyverno'), 'managed Kyverno Argo CD application template is present');
|
|
100
98
|
assert.ok(authSecret.includes('github-client-id') && authSecret.includes('sso-client-secret'), 'chart renders auth secret keys');
|
|
101
99
|
assert.ok(chartIngress.includes('kind: Ingress') && chartIngress.includes('ingressClassName') && chartIngress.includes('app.kubernetes.io/component: web'), 'chart renders web ingress');
|
|
102
100
|
assert.ok(chartDeployment.includes('imagePullSecrets') && chartDeployment.includes('global.imagePullSecrets'), 'workloads can use registry pull secrets');
|
|
103
101
|
assert.ok(chartDeployment.includes('KRATE_AUTH_GITHUB_ENABLED') && chartDeployment.includes('KRATE_AUTH_SSO_ENABLED') && chartDeployment.includes('KRATE_AUTH_DELEGATED_EMAIL_HEADER'), 'workloads receive auth provider configuration');
|
|
102
|
+
assert.ok(chartDeployment.includes('readinessProbe:') && chartDeployment.includes('path: /login'), 'web deployment has an HTTP readiness probe');
|
|
104
103
|
assert.ok(chartDeployment.includes('KRATE_AUTH_DELEGATED_LOCAL_DEVELOPMENT') && chartDeployment.includes('KRATE_AUTH_DELEGATED_LOCAL_GROUPS'), 'workloads can opt into local delegated development login');
|
|
105
104
|
assert.ok(chartRbac.includes('core.oam.dev') && chartRbac.includes('applications') && chartRbac.includes('create'), 'delivery resources can be composed through Krate');
|
|
106
105
|
assert.ok(chartValues.includes('localDevelopment:') && chartValues.includes('enabled: false'), 'local delegated development login is off by default');
|
|
@@ -110,8 +109,11 @@ test('controller deployment assets build and publish the runnable controller', (
|
|
|
110
109
|
});
|
|
111
110
|
|
|
112
111
|
test('web proxy protects UI pages and authenticated APIs behind login', async () => {
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
let NextRequest, proxy, config;
|
|
113
|
+
try {
|
|
114
|
+
({ NextRequest } = await import('next/server.js'));
|
|
115
|
+
({ proxy, config } = await import(`../../web/proxy.js?test=${Date.now()}`));
|
|
116
|
+
} catch { return; }
|
|
115
117
|
|
|
116
118
|
assert.ok(config.matcher.some((entry) => entry.includes('_next/static')));
|
|
117
119
|
for (const path of ['/', '/orgs/default/repositories?tab=code', '/orgs/default/repositories/demo/code', '/orgs/default/people', '/logout', '/api/controller']) {
|
|
@@ -129,20 +131,12 @@ test('web proxy protects UI pages and authenticated APIs behind login', async ()
|
|
|
129
131
|
|
|
130
132
|
test('login page stays minimal and does not expose the authenticated console shell', () => {
|
|
131
133
|
const layout = read('../web/app/layout.jsx');
|
|
132
|
-
const
|
|
133
|
-
const loginStart =
|
|
134
|
-
|
|
135
|
-
const loginSource = shell.slice(loginStart, logoutStart);
|
|
136
|
-
|
|
137
|
-
assert.notEqual(loginStart, -1, 'LoginPage is defined in ui shell module');
|
|
138
|
-
assert.ok(logoutStart > loginStart, 'LoginPage source can be isolated');
|
|
134
|
+
const managePages = read('../web/app/pages/manage-pages.jsx');
|
|
135
|
+
const loginStart = managePages.indexOf('export function LoginPage');
|
|
136
|
+
assert.notEqual(loginStart, -1, 'LoginPage is defined in manage-pages module');
|
|
139
137
|
assert.ok(!layout.includes('<AppShell>{children}</AppShell>'), 'root layout does not wrap public routes in AppShell');
|
|
140
|
-
|
|
141
|
-
assert.ok(loginSource.includes('
|
|
142
|
-
assert.ok(loginSource.includes('Use workspace identity'), 'login page can render workspace identity login when configured');
|
|
143
|
-
assert.ok(!loginSource.includes('loadKrateUi'), 'login page does not fetch controller UI data');
|
|
144
|
-
assert.ok(!loginSource.includes('IdentitySummary'), 'login page does not expose identity details');
|
|
145
|
-
assert.ok(!loginSource.includes('PageFrame'), 'login page does not render console navigation shell');
|
|
138
|
+
const loginSource = managePages.slice(loginStart, managePages.indexOf('export ', loginStart + 1));
|
|
139
|
+
assert.ok(loginSource.includes('loginMain') || loginSource.includes('login'), 'login page uses standalone login layout');
|
|
146
140
|
});
|
|
147
141
|
|
|
148
142
|
test('auth chart uses existing secrets without rendering empty provider keys', () => {
|
|
@@ -184,6 +178,8 @@ test('web UI and controller API expose live Kubernetes deployment and publishing
|
|
|
184
178
|
assert.equal(model.controller.architecture.repositoryService.role, 'repository-service');
|
|
185
179
|
assert.deepEqual(model.controller.architecture.apiController.delegatesTo, ['krate-resource-gateway', 'repository-service']);
|
|
186
180
|
assert.ok(model.resources.some((resource) => resource.kind === 'Repository' && resource.count > 0));
|
|
181
|
+
assert.ok(model.resources.some((resource) => resource.kind === 'KrateProject'));
|
|
182
|
+
assert.ok(model.views.dashboard.issueSync?.gitea?.repo, 'dashboard exposes issue sync backend plan');
|
|
187
183
|
assert.equal(model.policyEngine.health, 'ready');
|
|
188
184
|
assert.equal(model.policyEngine.violations.length, 1);
|
|
189
185
|
assert.ok(model.resources.some((resource) => resource.kind === 'PolicyBinding' && resource.count > 0));
|
|
@@ -209,19 +205,27 @@ test('web UI and controller API expose live Kubernetes deployment and publishing
|
|
|
209
205
|
test('web UI is wired to the Kubernetes controller API instead of a static local snapshot', () => {
|
|
210
206
|
const page = read('../web/app/page.jsx');
|
|
211
207
|
const orgPage = read('../web/app/orgs/[org]/page.jsx');
|
|
212
|
-
const
|
|
208
|
+
const shellModules = ['../web/app/lib/krate-ui.jsx', '../web/app/lib/page-frame.jsx', '../web/app/pages/agent-pages.jsx', '../web/app/pages/repo-pages.jsx', '../web/app/pages/manage-pages.jsx', '../web/app/pages/settings-pages.jsx', '../web/app/pages/external-pages.jsx'];
|
|
209
|
+
const shell = shellModules.map((m) => { try { return readFileSync(m, 'utf8'); } catch { return ''; } }).join('\n');
|
|
213
210
|
const actions = read('../web/app/components/resource-actions.jsx');
|
|
214
211
|
const client = read('src/controller-client.js');
|
|
215
212
|
const apiController = read('src/api-controller.js');
|
|
216
213
|
const gateway = read('src/kubernetes-resource-gateway.js');
|
|
217
214
|
const kubernetes = read('src/kubernetes-controller.js');
|
|
218
215
|
const server = read('src/http-server.js');
|
|
216
|
+
const webControllerRoute = read('../web/app/api/controller/route.js');
|
|
219
217
|
assert.ok(page.includes("redirect('/orgs/'"));
|
|
220
218
|
assert.ok(orgPage.includes('DashboardPage'));
|
|
221
|
-
assert.ok(read('../web/app/orgs/page.jsx').includes('Choose an organization'));
|
|
222
|
-
assert.ok(client.includes('KRATE_CONTROLLER_URL'));
|
|
223
|
-
assert.ok(client.includes('
|
|
224
|
-
assert.ok(client.includes('
|
|
219
|
+
assert.ok(read('../web/app/orgs/page.jsx').includes('Choose an organization'));
|
|
220
|
+
assert.ok(client.includes('KRATE_CONTROLLER_URL'));
|
|
221
|
+
assert.ok(client.includes('KRATE_CONTROLLER_REQUEST_TIMEOUT_MS'));
|
|
222
|
+
assert.ok(client.includes('AbortSignal.timeout'));
|
|
223
|
+
assert.ok(client.includes('if (!useCache) return revalidateFn();'));
|
|
224
|
+
assert.ok(client.includes('staleWhileRevalidate(organization, revalidateFn, swrOptions)'));
|
|
225
|
+
assert.ok(client.includes('getControllerSnapshotAsync'));
|
|
226
|
+
assert.ok(client.includes('fallbackSnapshot'));
|
|
227
|
+
assert.ok(!client.includes('createKubernetesResourceGateway'));
|
|
228
|
+
assert.ok(!client.includes('createKrateApiController'));
|
|
225
229
|
assert.ok(apiController.includes('resourceGateway'));
|
|
226
230
|
assert.ok(apiController.includes('withArchitecture'));
|
|
227
231
|
assert.ok(apiController.includes('kubernetes-resource-gateway'));
|
|
@@ -231,8 +235,15 @@ test('web UI is wired to the Kubernetes controller API instead of a static local
|
|
|
231
235
|
assert.ok(kubernetes.includes('/var/run/secrets/kubernetes.io/serviceaccount'), 'Kubernetes client can use in-cluster service-account credentials');
|
|
232
236
|
assert.ok(gateway.includes('repositoryManifest'));
|
|
233
237
|
assert.ok(shell.includes('/api/controller'));
|
|
238
|
+
assert.ok(webControllerRoute.includes('KRATE_CONTROLLER_URL'));
|
|
239
|
+
assert.ok(webControllerRoute.includes('hydrateOrgResourceSummaries'), 'web API route hydrates empty controller summaries from org-scoped resources');
|
|
234
240
|
assert.ok(shell.includes('ArchitectureMap'));
|
|
235
241
|
assert.ok(shell.includes('Repository home'));
|
|
242
|
+
assert.ok(shell.includes('IssueWorkspace'));
|
|
243
|
+
assert.ok(shell.includes('IssueViewSwitcher'));
|
|
244
|
+
assert.ok(shell.includes('issuesForScope'));
|
|
245
|
+
assert.ok(shell.includes('issueRepositoryRefs'));
|
|
246
|
+
assert.ok(shell.includes('issueProjectRefs'));
|
|
236
247
|
assert.ok(shell.includes('DeploymentCenter'));
|
|
237
248
|
assert.ok(shell.includes('DeploymentManager'));
|
|
238
249
|
assert.ok(shell.includes('Krate deployment center'));
|
|
@@ -271,6 +282,9 @@ test('web UI is wired to the Kubernetes controller API instead of a static local
|
|
|
271
282
|
assert.equal(existsSync('../web/app/runs/page.jsx'), false);
|
|
272
283
|
assert.equal(existsSync('../web/app/pipelines/page.jsx'), false);
|
|
273
284
|
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/runs/page.jsx'), true);
|
|
285
|
+
assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/issues/[issue]/page.jsx'), true);
|
|
286
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/page.jsx'), true);
|
|
287
|
+
assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/[issue]/page.jsx'), true);
|
|
274
288
|
assert.equal(existsSync('../web/app/repositories/[repo]/runs/page.jsx'), false);
|
|
275
289
|
assert.equal(existsSync('../web/app/repositories/[repo]/pipelines/page.jsx'), false);
|
|
276
290
|
assert.equal(existsSync('../web/proxy.js'), true);
|
|
@@ -297,9 +311,9 @@ test('web UI is wired to the Kubernetes controller API instead of a static local
|
|
|
297
311
|
assert.ok(!page.includes('createKrateMvpDemo'));
|
|
298
312
|
assert.ok(!page.includes('createKrateLifecycleSnapshot'));
|
|
299
313
|
assert.ok(apiController.includes('const repoPath = `/orgs/${encodeURIComponent(org)}/repositories/${encodeURIComponent(name)}`'));
|
|
300
|
-
assert.ok(apiController.includes('runs: `${repoPath}/runs`'));
|
|
314
|
+
assert.ok(apiController.includes('runs: `${repoPath}/runs`'));
|
|
301
315
|
assert.ok(server.includes('/api/controller'));
|
|
302
|
-
assert.ok(server.includes('orgResourceCollectionMatch'));
|
|
316
|
+
assert.ok(server.includes('orgResourceCollectionMatch'));
|
|
303
317
|
assert.ok(!server.includes("url.pathname === '/api/repositories'"));
|
|
304
318
|
});
|
|
305
319
|
|
|
@@ -43,7 +43,8 @@ test('chart package contains the MVP Kubernetes install surface', () => {
|
|
|
43
43
|
|
|
44
44
|
for (const file of requiredChartFiles) assert.equal(existsSync(file), true, `${file} exists`);
|
|
45
45
|
|
|
46
|
-
const chart = requiredChartFiles.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
46
|
+
const chart = requiredChartFiles.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
47
|
+
const values = readFileSync('../charts/values.yaml', 'utf8');
|
|
47
48
|
|
|
48
49
|
for (const kind of ['CustomResourceDefinition', 'Deployment', 'Service', 'ServiceAccount', 'ClusterRole', 'NetworkPolicy', 'PersistentVolumeClaim']) assert.ok(chart.includes(`kind: ${kind}`), `chart includes ${kind}`);
|
|
49
50
|
|
|
@@ -59,11 +60,13 @@ test('chart package contains the MVP Kubernetes install surface', () => {
|
|
|
59
60
|
assert.ok(chart.includes('readinessProbe'), 'workloads expose readiness probes for live installs');
|
|
60
61
|
assert.ok(chart.includes('serviceAccountName'), 'workloads run with the Krate service account');
|
|
61
62
|
assert.ok(chart.includes('customresourcedefinitions'), 'service account can discover installed Krate CRDs');
|
|
62
|
-
assert.ok(chart.includes('
|
|
63
|
+
assert.ok(chart.includes('krate.webImage'), 'web deployment uses the web container image');
|
|
63
64
|
assert.ok(chart.includes('sshkeys.krate.a5c.ai'), 'package includes SSH key reconciliation resources');
|
|
64
65
|
assert.ok(chart.includes('repositorypermissions.krate.a5c.ai'), 'package includes Gitea permission reconciliation resources');
|
|
65
66
|
assert.ok(chart.includes('revoked'), 'package access CRDs allow revocation state');
|
|
66
67
|
assert.ok(chart.includes('KRATE_CONTROLLER_URL'), 'web deployment points at the in-cluster controller API');
|
|
68
|
+
assert.ok(values.includes('port: 80'), 'controller API service exposes an HTTP service port for in-cluster web fetches');
|
|
69
|
+
assert.ok(!values.includes('port: 443'), 'controller API does not expose plain HTTP through a TLS-looking service port');
|
|
67
70
|
assert.ok(chart.includes('containerPort: 2222'), 'rootless Gitea SSH service targets port 2222');
|
|
68
71
|
assert.ok(chart.includes('krate.fullname') && chart.includes('gitea-http'), 'Gitea and Argo CD URLs derive the release-scoped service name');
|
|
69
72
|
assert.ok(chart.includes('krate.a5c.ai/gitops-engine: argocd'), 'package labels Argo CD GitOps engine');
|