@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,361 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentAdapterController, validateAgentAdapter, createResource, AGENT_ADAPTER_CONTROLLER_BOUNDARY } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Acceptance criteria: Slice 1.2a — Agent Adapter Controller
|
|
7
|
+
//
|
|
8
|
+
// An AgentAdapter defines how an agent connects to a model provider.
|
|
9
|
+
// It specifies adapter name/type, supported transport protocols
|
|
10
|
+
// (stdio, http, websocket), a capabilities matrix, and a health check
|
|
11
|
+
// endpoint/method.
|
|
12
|
+
//
|
|
13
|
+
// All tests in this file are expected to FAIL until the controller is
|
|
14
|
+
// implemented and exported from src/index.js.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const VALID_TRANSPORTS = ['stdio', 'http', 'websocket', 'unix'];
|
|
18
|
+
const VALID_ADAPTER_TYPES = ['subprocess', 'remote', 'programmatic'];
|
|
19
|
+
|
|
20
|
+
function makeAdapter(name, overrides = {}) {
|
|
21
|
+
return createResource('AgentAdapter', { name, namespace: 'krate-org-default' }, {
|
|
22
|
+
organizationRef: 'default',
|
|
23
|
+
adapterType: 'subprocess',
|
|
24
|
+
transport: 'stdio',
|
|
25
|
+
capabilities: ['tool-use', 'streaming'],
|
|
26
|
+
...overrides
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// 1. Factory and shape
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
test('createAgentAdapterController returns a controller with validate method', () => {
|
|
35
|
+
const controller = createAgentAdapterController();
|
|
36
|
+
assert.ok(controller, 'controller must be truthy');
|
|
37
|
+
assert.equal(typeof controller.validate, 'function', 'controller must expose a validate method');
|
|
38
|
+
assert.equal(controller.role, 'agent-adapter-controller', 'controller must declare its role');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// 2. validate — happy path
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
test('validate accepts a valid adapter with name, type, and transport', () => {
|
|
46
|
+
const controller = createAgentAdapterController();
|
|
47
|
+
const adapter = makeAdapter('claude-code-adapter');
|
|
48
|
+
const result = controller.validate(adapter);
|
|
49
|
+
|
|
50
|
+
assert.equal(result.valid, true, 'valid adapter must pass validation');
|
|
51
|
+
assert.ok(Array.isArray(result.errors), 'result must contain an errors array');
|
|
52
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty for a valid adapter');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// 3. validate — missing name
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
test('validate rejects adapter with missing name', () => {
|
|
60
|
+
const controller = createAgentAdapterController();
|
|
61
|
+
// Build a raw object without metadata.name to bypass createResource guard
|
|
62
|
+
const adapter = {
|
|
63
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
64
|
+
kind: 'AgentAdapter',
|
|
65
|
+
metadata: { namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
66
|
+
spec: { organizationRef: 'default', adapterType: 'subprocess', transport: 'stdio', capabilities: ['tool-use'] },
|
|
67
|
+
status: {}
|
|
68
|
+
};
|
|
69
|
+
const result = controller.validate(adapter);
|
|
70
|
+
|
|
71
|
+
assert.equal(result.valid, false, 'adapter without a name must fail validation');
|
|
72
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
73
|
+
assert.ok(
|
|
74
|
+
result.errors.some((e) => /name/i.test(e)),
|
|
75
|
+
'at least one error must mention "name"'
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// 4. validate — invalid transport type
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
test('validate rejects adapter with invalid transport type', () => {
|
|
84
|
+
const controller = createAgentAdapterController();
|
|
85
|
+
const adapter = makeAdapter('bad-transport-adapter', { transport: 'grpc' });
|
|
86
|
+
const result = controller.validate(adapter);
|
|
87
|
+
|
|
88
|
+
assert.equal(result.valid, false, 'adapter with unsupported transport must fail validation');
|
|
89
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
90
|
+
assert.ok(
|
|
91
|
+
result.errors.some((e) => /transport/i.test(e)),
|
|
92
|
+
'at least one error must mention "transport"'
|
|
93
|
+
);
|
|
94
|
+
// Confirm that valid transports are enumerated in the error message
|
|
95
|
+
assert.ok(
|
|
96
|
+
result.errors.some((e) => VALID_TRANSPORTS.some((t) => e.includes(t))),
|
|
97
|
+
'error must enumerate valid transport types'
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// 5. validate — empty capabilities array
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
test('validate rejects adapter with empty capabilities array', () => {
|
|
106
|
+
const controller = createAgentAdapterController();
|
|
107
|
+
const adapter = makeAdapter('no-caps-adapter', { capabilities: [] });
|
|
108
|
+
const result = controller.validate(adapter);
|
|
109
|
+
|
|
110
|
+
assert.equal(result.valid, false, 'adapter with empty capabilities must fail validation');
|
|
111
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
112
|
+
assert.ok(
|
|
113
|
+
result.errors.some((e) => /capabilities/i.test(e)),
|
|
114
|
+
'at least one error must mention "capabilities"'
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// 6. getCapabilities — capabilities matrix
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
test('getCapabilities returns the capabilities matrix for an adapter', () => {
|
|
123
|
+
const controller = createAgentAdapterController();
|
|
124
|
+
const adapter = makeAdapter('full-adapter', {
|
|
125
|
+
capabilities: ['tool-use', 'streaming', 'file-access']
|
|
126
|
+
});
|
|
127
|
+
const matrix = controller.getCapabilities(adapter);
|
|
128
|
+
|
|
129
|
+
assert.ok(matrix, 'getCapabilities must return a value');
|
|
130
|
+
assert.ok(Array.isArray(matrix.supported), 'matrix must have a supported array');
|
|
131
|
+
assert.ok(matrix.supported.includes('tool-use'), 'supported must include tool-use');
|
|
132
|
+
assert.ok(matrix.supported.includes('streaming'), 'supported must include streaming');
|
|
133
|
+
assert.ok(matrix.supported.includes('file-access'), 'supported must include file-access');
|
|
134
|
+
assert.equal(matrix.adapterName, adapter.metadata.name, 'matrix must carry the adapter name');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// 7. getSupportedTransports — transport enumeration
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
test('getSupportedTransports returns the transport types', () => {
|
|
142
|
+
const controller = createAgentAdapterController();
|
|
143
|
+
const transports = controller.getSupportedTransports();
|
|
144
|
+
|
|
145
|
+
assert.ok(Array.isArray(transports), 'getSupportedTransports must return an array');
|
|
146
|
+
assert.equal(transports.length, 4, 'exactly four transport types must be supported');
|
|
147
|
+
assert.ok(transports.includes('stdio'), 'transports must include stdio');
|
|
148
|
+
assert.ok(transports.includes('http'), 'transports must include http');
|
|
149
|
+
assert.ok(transports.includes('websocket'), 'transports must include websocket');
|
|
150
|
+
assert.ok(transports.includes('unix'), 'transports must include unix');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// 8. healthCheck — stub result when no endpoint configured
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
test('healthCheck returns a result with status "unknown" when no endpoint configured', async () => {
|
|
158
|
+
const controller = createAgentAdapterController();
|
|
159
|
+
const adapter = makeAdapter('no-endpoint-adapter');
|
|
160
|
+
// Adapter has no healthEndpoint in spec
|
|
161
|
+
const result = await controller.healthCheck(adapter);
|
|
162
|
+
|
|
163
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
164
|
+
assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint is configured');
|
|
165
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
166
|
+
assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// 9. validateAgentAdapter — standalone export follows existing pattern
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
test('validateAgentAdapter exported from controller follows existing pattern', () => {
|
|
174
|
+
assert.equal(typeof validateAgentAdapter, 'function', 'validateAgentAdapter must be a named export');
|
|
175
|
+
|
|
176
|
+
const adapter = makeAdapter('standalone-validate-adapter');
|
|
177
|
+
const result = validateAgentAdapter(adapter);
|
|
178
|
+
|
|
179
|
+
assert.ok(result, 'validateAgentAdapter must return a result');
|
|
180
|
+
assert.ok('valid' in result, 'result must have a valid property');
|
|
181
|
+
assert.ok(Array.isArray(result.errors), 'result must have an errors array');
|
|
182
|
+
assert.equal(result.valid, true, 'a fully-specified adapter must pass standalone validation');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// 10. validate — missing adapterType
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
test('validate rejects adapter with missing adapterType', () => {
|
|
190
|
+
const controller = createAgentAdapterController();
|
|
191
|
+
const adapter = makeAdapter('no-type-adapter', { adapterType: undefined });
|
|
192
|
+
// Remove adapterType from spec entirely
|
|
193
|
+
delete adapter.spec.adapterType;
|
|
194
|
+
const result = controller.validate(adapter);
|
|
195
|
+
|
|
196
|
+
assert.equal(result.valid, false, 'adapter without adapterType must fail validation');
|
|
197
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
198
|
+
assert.ok(
|
|
199
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
200
|
+
'at least one error must mention "adapterType"'
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// 11. validate — invalid adapterType ('garbage')
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
test('validate rejects adapter with invalid adapterType', () => {
|
|
209
|
+
const controller = createAgentAdapterController();
|
|
210
|
+
const adapter = makeAdapter('bad-type-adapter', { adapterType: 'garbage' });
|
|
211
|
+
const result = controller.validate(adapter);
|
|
212
|
+
|
|
213
|
+
assert.equal(result.valid, false, 'adapter with unsupported adapterType must fail validation');
|
|
214
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
215
|
+
assert.ok(
|
|
216
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
217
|
+
'at least one error must mention "adapterType"'
|
|
218
|
+
);
|
|
219
|
+
assert.ok(
|
|
220
|
+
result.errors.some((e) => VALID_ADAPTER_TYPES.some((t) => e.includes(t))),
|
|
221
|
+
'error must enumerate valid adapter types'
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// 12. validate — unix transport is valid
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
test('validate accepts adapter with unix transport', () => {
|
|
230
|
+
const controller = createAgentAdapterController();
|
|
231
|
+
const adapter = makeAdapter('unix-transport-adapter', { transport: 'unix' });
|
|
232
|
+
const result = controller.validate(adapter);
|
|
233
|
+
|
|
234
|
+
assert.equal(result.valid, true, 'adapter with unix transport must pass validation');
|
|
235
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// 13. healthCheck returns 'not-implemented' when healthEndpoint IS configured
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
test('healthCheck performs real HTTP check when healthEndpoint is configured', async () => {
|
|
243
|
+
// Use a mock fetch to avoid real network calls in unit tests
|
|
244
|
+
const mockFetch = async () => ({ ok: true, status: 200 });
|
|
245
|
+
const controller = createAgentAdapterController({ fetch: mockFetch });
|
|
246
|
+
const adapter = makeAdapter('endpoint-adapter', { healthEndpoint: 'http://localhost:9090/health' });
|
|
247
|
+
const result = await controller.healthCheck(adapter);
|
|
248
|
+
|
|
249
|
+
assert.ok(result, 'healthCheck must return a result');
|
|
250
|
+
assert.equal(result.status, 'healthy', 'status must be "healthy" when endpoint fetch succeeds');
|
|
251
|
+
assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
|
|
252
|
+
assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// 14. validate / getCapabilities / healthCheck reject null resource
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
test('validate rejects null resource with a clear error', () => {
|
|
260
|
+
const controller = createAgentAdapterController();
|
|
261
|
+
const result = controller.validate(null);
|
|
262
|
+
|
|
263
|
+
assert.equal(result.valid, false, 'null resource must fail validation');
|
|
264
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
265
|
+
assert.ok(
|
|
266
|
+
result.errors.some((e) => /null|undefined/i.test(e)),
|
|
267
|
+
'error must mention null or undefined'
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('getCapabilities throws on null resource', () => {
|
|
272
|
+
const controller = createAgentAdapterController();
|
|
273
|
+
assert.throws(
|
|
274
|
+
() => controller.getCapabilities(null),
|
|
275
|
+
/null|undefined/i,
|
|
276
|
+
'getCapabilities must throw on null resource'
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('healthCheck rejects on null resource', async () => {
|
|
281
|
+
const controller = createAgentAdapterController();
|
|
282
|
+
await assert.rejects(
|
|
283
|
+
() => controller.healthCheck(null),
|
|
284
|
+
/null|undefined/i,
|
|
285
|
+
'healthCheck must reject on null resource'
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// 15. getSupportedAdapterTypes — adapter type enumeration
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
|
|
293
|
+
test('getSupportedAdapterTypes returns the valid adapter types array', () => {
|
|
294
|
+
const controller = createAgentAdapterController();
|
|
295
|
+
const types = controller.getSupportedAdapterTypes();
|
|
296
|
+
|
|
297
|
+
assert.ok(Array.isArray(types), 'getSupportedAdapterTypes must return an array');
|
|
298
|
+
assert.equal(types.length, VALID_ADAPTER_TYPES.length, 'must return all valid adapter types');
|
|
299
|
+
assert.ok(types.includes('subprocess'), 'adapter types must include subprocess');
|
|
300
|
+
assert.ok(types.includes('remote'), 'adapter types must include remote');
|
|
301
|
+
assert.ok(types.includes('programmatic'), 'adapter types must include programmatic');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// 16. getCapabilities with undefined spec (resource with no spec key)
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
test('getCapabilities handles resource with no spec key gracefully', () => {
|
|
309
|
+
const controller = createAgentAdapterController();
|
|
310
|
+
const resource = {
|
|
311
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
312
|
+
kind: 'AgentAdapter',
|
|
313
|
+
metadata: { name: 'spec-less-adapter', namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
314
|
+
status: {}
|
|
315
|
+
// no spec key at all
|
|
316
|
+
};
|
|
317
|
+
const matrix = controller.getCapabilities(resource);
|
|
318
|
+
|
|
319
|
+
assert.ok(matrix, 'getCapabilities must return a value even with no spec');
|
|
320
|
+
assert.ok(Array.isArray(matrix.supported), 'supported must be an array');
|
|
321
|
+
assert.equal(matrix.supported.length, 0, 'supported must be empty when spec is absent');
|
|
322
|
+
assert.equal(matrix.adapterName, 'spec-less-adapter', 'adapterName must still be populated');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// 17. validate — simultaneous adapterType AND transport errors accumulate
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
test('validate accumulates all errors when both adapterType and transport are invalid', () => {
|
|
330
|
+
const controller = createAgentAdapterController();
|
|
331
|
+
const adapter = makeAdapter('double-bad-adapter', { adapterType: 'bad-type', transport: 'grpc' });
|
|
332
|
+
const result = controller.validate(adapter);
|
|
333
|
+
|
|
334
|
+
assert.equal(result.valid, false, 'adapter with multiple invalid fields must fail validation');
|
|
335
|
+
assert.ok(
|
|
336
|
+
result.errors.some((e) => /adapterType/i.test(e)),
|
|
337
|
+
'errors must include an adapterType error'
|
|
338
|
+
);
|
|
339
|
+
assert.ok(
|
|
340
|
+
result.errors.some((e) => /transport/i.test(e)),
|
|
341
|
+
'errors must include a transport error'
|
|
342
|
+
);
|
|
343
|
+
assert.ok(result.errors.length >= 2, 'must accumulate at least two errors');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// 18. BOUNDARY object export
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
test('AGENT_ADAPTER_CONTROLLER_BOUNDARY is exported and has correct role', () => {
|
|
351
|
+
assert.ok(AGENT_ADAPTER_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
|
|
352
|
+
assert.equal(
|
|
353
|
+
AGENT_ADAPTER_CONTROLLER_BOUNDARY.role,
|
|
354
|
+
'agent-adapter-controller',
|
|
355
|
+
'BOUNDARY role must be "agent-adapter-controller"'
|
|
356
|
+
);
|
|
357
|
+
assert.ok(
|
|
358
|
+
Array.isArray(AGENT_ADAPTER_CONTROLLER_BOUNDARY.owns),
|
|
359
|
+
'BOUNDARY must declare owned concerns'
|
|
360
|
+
);
|
|
361
|
+
});
|
|
@@ -53,6 +53,15 @@ test('Successful dispatch with Agent Mux available', async () => {
|
|
|
53
53
|
role: 'agent-mux-client',
|
|
54
54
|
isAvailable() { return true; },
|
|
55
55
|
async launchSession() { return { runId: `amux-${Date.now()}`, sessionId: `session-${Date.now()}` }; },
|
|
56
|
+
subscribeToEvents(runId, cb) { return { abort() {} }; },
|
|
57
|
+
reconcileTranscript(sessionId, events, opts) {
|
|
58
|
+
return createResource('AgentSessionTranscript', { name: `transcript-${sessionId}`, namespace: opts?.namespace || 'default' }, {
|
|
59
|
+
organizationRef: opts?.organizationRef || 'default',
|
|
60
|
+
sessionRef: sessionId,
|
|
61
|
+
messages: [],
|
|
62
|
+
cost: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
63
|
+
}, { phase: 'Reconciled', reconciledAt: new Date().toISOString() });
|
|
64
|
+
},
|
|
56
65
|
};
|
|
57
66
|
const resources = buildValidResources('dispatch-stack');
|
|
58
67
|
const controller = createAgentDispatchController({ agentMuxClient: muxClient });
|
|
@@ -174,3 +183,133 @@ test('Context bundle referenced correctly', async () => {
|
|
|
174
183
|
'Run contextBundleRef should match context bundle name'
|
|
175
184
|
);
|
|
176
185
|
});
|
|
186
|
+
|
|
187
|
+
function makeMemoryRepository(name) {
|
|
188
|
+
return createResource('AgentMemoryRepository', { name, namespace: 'krate-org-default' }, {
|
|
189
|
+
organizationRef: 'default',
|
|
190
|
+
repositoryRef: 'memory-repo',
|
|
191
|
+
defaultBranch: 'main',
|
|
192
|
+
layoutProfile: 'standard',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
test('Dispatch with AgentMemoryRepository creates memorySnapshot', async () => {
|
|
197
|
+
const muxClient = createAgentMuxClient({ gateway: '', enabled: false });
|
|
198
|
+
const resources = {
|
|
199
|
+
...buildValidResources('mem-stack'),
|
|
200
|
+
AgentMemoryRepository: [makeMemoryRepository('org-memory')],
|
|
201
|
+
};
|
|
202
|
+
const controller = createAgentDispatchController({ agentMuxClient: muxClient });
|
|
203
|
+
|
|
204
|
+
const result = await controller.createManualDispatch({
|
|
205
|
+
repository: 'test-repo',
|
|
206
|
+
ref: 'main',
|
|
207
|
+
agentStack: 'mem-stack',
|
|
208
|
+
actor: 'test-user',
|
|
209
|
+
namespace: 'krate-org-default',
|
|
210
|
+
organizationRef: 'default',
|
|
211
|
+
resources,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
assert.equal(result.error, false, 'Dispatch should succeed');
|
|
215
|
+
assert.ok(result.memorySnapshot, 'Result should include memorySnapshot');
|
|
216
|
+
assert.equal(result.memorySnapshot.kind, 'AgentMemorySnapshot', 'memorySnapshot should be an AgentMemorySnapshot');
|
|
217
|
+
assert.equal(result.memorySnapshot.spec.memoryRepository, 'org-memory', 'memorySnapshot should reference the memory repo');
|
|
218
|
+
assert.equal(result.memorySnapshot.status.phase, 'Pinned', 'memorySnapshot should be Pinned');
|
|
219
|
+
assert.equal(result.run.spec.memorySnapshotRef, result.memorySnapshot.metadata.name, 'Run should reference memorySnapshot');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('Dispatch without memory repos has null memorySnapshot', async () => {
|
|
223
|
+
const muxClient = createAgentMuxClient({ gateway: '', enabled: false });
|
|
224
|
+
const resources = buildValidResources('no-mem-stack');
|
|
225
|
+
const controller = createAgentDispatchController({ agentMuxClient: muxClient });
|
|
226
|
+
|
|
227
|
+
const result = await controller.createManualDispatch({
|
|
228
|
+
repository: 'test-repo',
|
|
229
|
+
ref: 'main',
|
|
230
|
+
agentStack: 'no-mem-stack',
|
|
231
|
+
actor: 'test-user',
|
|
232
|
+
namespace: 'krate-org-default',
|
|
233
|
+
organizationRef: 'default',
|
|
234
|
+
resources,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
assert.equal(result.error, false, 'Dispatch should succeed');
|
|
238
|
+
assert.equal(result.memorySnapshot, null, 'memorySnapshot should be null when no AgentMemoryRepository');
|
|
239
|
+
assert.equal(result.run.spec.memorySnapshotRef, undefined, 'Run should not have memorySnapshotRef');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Dispatch with requires-approval returns early with awaitingApproval', async () => {
|
|
243
|
+
// Build resources where the secret grant has requiredApproval set,
|
|
244
|
+
// which causes the permission reviewer to return 'requires-approval'
|
|
245
|
+
const stack = makeStack('approval-stack');
|
|
246
|
+
const resources = {
|
|
247
|
+
AgentStack: [stack],
|
|
248
|
+
AgentServiceAccount: [makeServiceAccount('sa-default')],
|
|
249
|
+
AgentRoleBinding: [makeRoleBinding('rb-1', 'sa-default')],
|
|
250
|
+
AgentSecretGrant: [makeSecretGrant('sg-model', 'sa-default', 'model-provider')],
|
|
251
|
+
};
|
|
252
|
+
// Add requiredApproval to the secret grant so permission review returns 'requires-approval'
|
|
253
|
+
resources.AgentSecretGrant[0].spec.requiredApproval = 'manager';
|
|
254
|
+
|
|
255
|
+
const controller = createAgentDispatchController();
|
|
256
|
+
|
|
257
|
+
const result = await controller.createManualDispatch({
|
|
258
|
+
repository: 'test-repo',
|
|
259
|
+
ref: 'main',
|
|
260
|
+
agentStack: 'approval-stack',
|
|
261
|
+
actor: 'test-user',
|
|
262
|
+
namespace: 'krate-org-default',
|
|
263
|
+
organizationRef: 'default',
|
|
264
|
+
resources,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
assert.equal(result.error, false, 'Dispatch should not error — it is awaiting approval');
|
|
268
|
+
assert.equal(result.awaitingApproval, true, 'Result should indicate awaitingApproval');
|
|
269
|
+
assert.equal(result.run.status.phase, 'AwaitingApproval', 'Run phase should be AwaitingApproval');
|
|
270
|
+
assert.ok(result.approval, 'Result should include the approval resource');
|
|
271
|
+
assert.equal(result.approval.kind, 'AgentApproval', 'Approval should be an AgentApproval resource');
|
|
272
|
+
assert.equal(result.approval.status.phase, 'Pending', 'Approval should be in Pending phase');
|
|
273
|
+
assert.ok(result.permissionSnapshot, 'Result should include permissionSnapshot');
|
|
274
|
+
// No contextBundle or attempt since we returned early
|
|
275
|
+
assert.equal(result.contextBundle, undefined, 'No contextBundle when awaiting approval');
|
|
276
|
+
assert.equal(result.attempt, undefined, 'No attempt when awaiting approval');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test('Successful launch creates transcript ref on run', async () => {
|
|
280
|
+
const sessionId = `session-${Date.now()}`;
|
|
281
|
+
const runId = `amux-${Date.now()}`;
|
|
282
|
+
const muxClient = {
|
|
283
|
+
role: 'agent-mux-client',
|
|
284
|
+
isAvailable() { return true; },
|
|
285
|
+
async launchSession() { return { runId, sessionId }; },
|
|
286
|
+
subscribeToEvents(rid, cb) { return { abort() {} }; },
|
|
287
|
+
reconcileTranscript(sid, events, opts) {
|
|
288
|
+
return createResource('AgentSessionTranscript', { name: `transcript-${sid}`, namespace: opts?.namespace || 'default' }, {
|
|
289
|
+
organizationRef: opts?.organizationRef || 'default',
|
|
290
|
+
sessionRef: sid,
|
|
291
|
+
messages: [],
|
|
292
|
+
cost: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
293
|
+
}, { phase: 'Reconciled', reconciledAt: new Date().toISOString() });
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
const resources = buildValidResources('transcript-stack');
|
|
297
|
+
const controller = createAgentDispatchController({ agentMuxClient: muxClient });
|
|
298
|
+
|
|
299
|
+
const result = await controller.createManualDispatch({
|
|
300
|
+
repository: 'test-repo',
|
|
301
|
+
ref: 'main',
|
|
302
|
+
agentStack: 'transcript-stack',
|
|
303
|
+
actor: 'test-user',
|
|
304
|
+
namespace: 'krate-org-default',
|
|
305
|
+
organizationRef: 'default',
|
|
306
|
+
resources,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
assert.equal(result.error, false, 'Dispatch should succeed');
|
|
310
|
+
assert.equal(result.run.status.phase, 'Running', 'Run phase should be Running');
|
|
311
|
+
assert.ok(result.transcript, 'Result should include transcript');
|
|
312
|
+
assert.equal(result.transcript.kind, 'AgentSessionTranscript', 'Transcript should be AgentSessionTranscript');
|
|
313
|
+
assert.ok(result.run.status.transcriptRef, 'Run should have transcriptRef in status');
|
|
314
|
+
assert.equal(result.run.status.transcriptRef, result.transcript.metadata.name, 'Run transcriptRef should match transcript name');
|
|
315
|
+
});
|