@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,201 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createAgentSubagentController, AGENT_SUBAGENT_CONTROLLER_BOUNDARY, createResource } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
function makeSubagent(name, specOverrides = {}) {
|
|
6
|
+
return createResource('AgentSubagent', { name, namespace: 'krate-org-default' }, {
|
|
7
|
+
organizationRef: 'default',
|
|
8
|
+
rolePrompt: 'You are a specialized subagent',
|
|
9
|
+
taskKinds: ['code-review', 'linting'],
|
|
10
|
+
role: 'reviewer',
|
|
11
|
+
parentStackRef: 'parent-stack-1',
|
|
12
|
+
...specOverrides
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeParentStack(name) {
|
|
17
|
+
return createResource('AgentStack', { name, namespace: 'krate-org-default' }, {
|
|
18
|
+
organizationRef: 'default',
|
|
19
|
+
baseAgent: 'claude-code',
|
|
20
|
+
adapter: 'anthropic',
|
|
21
|
+
runtimeIdentity: { serviceAccountRef: 'sa-default' }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 1. createAgentSubagentController returns controller with validate, dispatch, getToolScope
|
|
26
|
+
test('createAgentSubagentController returns controller with expected methods', () => {
|
|
27
|
+
const controller = createAgentSubagentController();
|
|
28
|
+
assert.ok(controller, 'controller should be created');
|
|
29
|
+
assert.equal(typeof controller.validate, 'function', 'should have validate method');
|
|
30
|
+
assert.equal(typeof controller.dispatchSubagent, 'function', 'should have dispatchSubagent method');
|
|
31
|
+
assert.equal(typeof controller.getToolScope, 'function', 'should have getToolScope method');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 2. validate accepts valid subagent (name, orgRef, parentStackRef, role)
|
|
35
|
+
test('validate accepts valid subagent with name, orgRef, parentStackRef, role', () => {
|
|
36
|
+
const controller = createAgentSubagentController();
|
|
37
|
+
const subagent = makeSubagent('valid-sub');
|
|
38
|
+
const result = controller.validate(subagent);
|
|
39
|
+
assert.equal(result.valid, true, 'valid subagent should pass validation');
|
|
40
|
+
assert.equal(result.errors.length, 0, 'should have no errors');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 3. validate rejects missing name
|
|
44
|
+
test('validate rejects subagent missing metadata.name', () => {
|
|
45
|
+
const controller = createAgentSubagentController();
|
|
46
|
+
const subagent = makeSubagent('temp-name');
|
|
47
|
+
delete subagent.metadata.name;
|
|
48
|
+
const result = controller.validate(subagent);
|
|
49
|
+
assert.equal(result.valid, false, 'should be invalid without name');
|
|
50
|
+
assert.ok(result.errors.some(e => e.includes('name')), 'error should mention name');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 4. validate rejects missing parentStackRef
|
|
54
|
+
test('validate rejects subagent missing parentStackRef', () => {
|
|
55
|
+
const controller = createAgentSubagentController();
|
|
56
|
+
const subagent = makeSubagent('no-parent', { parentStackRef: undefined });
|
|
57
|
+
const result = controller.validate(subagent);
|
|
58
|
+
assert.equal(result.valid, false, 'should be invalid without parentStackRef');
|
|
59
|
+
assert.ok(result.errors.some(e => e.includes('parentStackRef')), 'error should mention parentStackRef');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 5. validate rejects missing role
|
|
63
|
+
test('validate rejects subagent missing role', () => {
|
|
64
|
+
const controller = createAgentSubagentController();
|
|
65
|
+
const subagent = makeSubagent('no-role', { role: undefined });
|
|
66
|
+
const result = controller.validate(subagent);
|
|
67
|
+
assert.equal(result.valid, false, 'should be invalid without role');
|
|
68
|
+
assert.ok(result.errors.some(e => e.includes('role')), 'error should mention role');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 6. getToolScope returns allowed tools from spec
|
|
72
|
+
test('getToolScope returns allowed tools list from spec.toolScope.allowed', () => {
|
|
73
|
+
const controller = createAgentSubagentController();
|
|
74
|
+
const subagent = makeSubagent('tool-sub', {
|
|
75
|
+
toolScope: { allowed: ['Read', 'Grep', 'Glob'], denied: ['Bash'] }
|
|
76
|
+
});
|
|
77
|
+
const scope = controller.getToolScope(subagent);
|
|
78
|
+
assert.deepEqual(scope.allowed, ['Read', 'Grep', 'Glob']);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 7. getToolScope returns all tools when no restriction set
|
|
82
|
+
test('getToolScope returns unrestricted scope when no toolScope set', () => {
|
|
83
|
+
const controller = createAgentSubagentController();
|
|
84
|
+
const subagent = makeSubagent('open-sub');
|
|
85
|
+
const scope = controller.getToolScope(subagent);
|
|
86
|
+
assert.equal(scope.unrestricted, true, 'should be unrestricted when no toolScope set');
|
|
87
|
+
assert.deepEqual(scope.allowed, [], 'allowed should be empty when unrestricted');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 8. getDeniedTools returns denied tools list
|
|
91
|
+
test('getDeniedTools returns denied tools from spec.toolScope.denied', () => {
|
|
92
|
+
const controller = createAgentSubagentController();
|
|
93
|
+
const subagent = makeSubagent('restricted-sub', {
|
|
94
|
+
toolScope: { allowed: ['Read'], denied: ['Bash', 'Write'] }
|
|
95
|
+
});
|
|
96
|
+
const denied = controller.getDeniedTools(subagent);
|
|
97
|
+
assert.deepEqual(denied, ['Bash', 'Write'], 'should return denied tools');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 9. dispatchSubagent creates a dispatch record with parentSessionRef
|
|
101
|
+
test('dispatchSubagent creates dispatch record with parentSessionRef', () => {
|
|
102
|
+
const controller = createAgentSubagentController();
|
|
103
|
+
const subagent = makeSubagent('dispatch-sub');
|
|
104
|
+
const parentStack = makeParentStack('parent-stack-1');
|
|
105
|
+
const result = controller.dispatchSubagent({
|
|
106
|
+
subagent,
|
|
107
|
+
parentSessionRef: 'session-parent-abc',
|
|
108
|
+
taskKind: 'code-review',
|
|
109
|
+
namespace: 'krate-org-default',
|
|
110
|
+
organizationRef: 'default',
|
|
111
|
+
resources: { AgentStack: [parentStack] }
|
|
112
|
+
});
|
|
113
|
+
assert.ok(result.dispatchRecord, 'should return a dispatch record');
|
|
114
|
+
assert.equal(result.dispatchRecord.spec.parentSessionRef, 'session-parent-abc', 'should record parentSessionRef');
|
|
115
|
+
assert.ok(result.dispatchRecord.metadata.name, 'dispatch record should have a name');
|
|
116
|
+
assert.equal(result.error, undefined, 'should have no error');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// 10. dispatchSubagent rejects when parent session not provided
|
|
120
|
+
test('dispatchSubagent rejects when parentSessionRef not provided', () => {
|
|
121
|
+
const controller = createAgentSubagentController();
|
|
122
|
+
const subagent = makeSubagent('dispatch-no-parent');
|
|
123
|
+
const result = controller.dispatchSubagent({
|
|
124
|
+
subagent,
|
|
125
|
+
parentSessionRef: undefined,
|
|
126
|
+
taskKind: 'code-review',
|
|
127
|
+
namespace: 'krate-org-default',
|
|
128
|
+
organizationRef: 'default',
|
|
129
|
+
resources: {}
|
|
130
|
+
});
|
|
131
|
+
assert.equal(result.error, true, 'should return error when no parentSessionRef');
|
|
132
|
+
assert.ok(result.reason.includes('parentSessionRef') || result.message.includes('parentSessionRef'), 'error should mention parentSessionRef');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 11. getSupervisionConfig returns supervision settings (monitorInterval, maxDuration, autoTerminate)
|
|
136
|
+
test('getSupervisionConfig returns configured supervision settings', () => {
|
|
137
|
+
const controller = createAgentSubagentController();
|
|
138
|
+
const subagent = makeSubagent('supervised-sub', {
|
|
139
|
+
supervision: {
|
|
140
|
+
monitorInterval: 30,
|
|
141
|
+
maxDuration: 3600,
|
|
142
|
+
autoTerminate: true
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
const config = controller.getSupervisionConfig(subagent);
|
|
146
|
+
assert.equal(config.monitorInterval, 30, 'should return monitorInterval');
|
|
147
|
+
assert.equal(config.maxDuration, 3600, 'should return maxDuration');
|
|
148
|
+
assert.equal(config.autoTerminate, true, 'should return autoTerminate');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// 12. getSupervisionConfig returns defaults when not configured
|
|
152
|
+
test('getSupervisionConfig returns default supervision settings when not configured', () => {
|
|
153
|
+
const controller = createAgentSubagentController();
|
|
154
|
+
const subagent = makeSubagent('default-supervision-sub');
|
|
155
|
+
const config = controller.getSupervisionConfig(subagent);
|
|
156
|
+
assert.ok(typeof config.monitorInterval === 'number', 'monitorInterval should be a number');
|
|
157
|
+
assert.ok(typeof config.maxDuration === 'number', 'maxDuration should be a number');
|
|
158
|
+
assert.ok(typeof config.autoTerminate === 'boolean', 'autoTerminate should be a boolean');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 13. validateTaskRouting accepts valid routing (role matches available subagents)
|
|
162
|
+
test('validateTaskRouting accepts routing when role matches available subagent', () => {
|
|
163
|
+
const controller = createAgentSubagentController();
|
|
164
|
+
const subagents = [
|
|
165
|
+
makeSubagent('sub-reviewer', { role: 'reviewer' }),
|
|
166
|
+
makeSubagent('sub-tester', { role: 'tester' })
|
|
167
|
+
];
|
|
168
|
+
const result = controller.validateTaskRouting({ role: 'reviewer', taskKind: 'code-review', subagents });
|
|
169
|
+
assert.equal(result.valid, true, 'routing to existing role should be valid');
|
|
170
|
+
assert.ok(result.matchedSubagent, 'should return the matched subagent');
|
|
171
|
+
assert.equal(result.matchedSubagent.metadata.name, 'sub-reviewer');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 14. validateTaskRouting rejects routing to non-existent role
|
|
175
|
+
test('validateTaskRouting rejects routing to non-existent role', () => {
|
|
176
|
+
const controller = createAgentSubagentController();
|
|
177
|
+
const subagents = [
|
|
178
|
+
makeSubagent('sub-reviewer', { role: 'reviewer' })
|
|
179
|
+
];
|
|
180
|
+
const result = controller.validateTaskRouting({ role: 'deployer', taskKind: 'deploy', subagents });
|
|
181
|
+
assert.equal(result.valid, false, 'routing to non-existent role should be invalid');
|
|
182
|
+
assert.ok(result.error.includes('deployer') || result.error.includes('role'), 'error should mention the missing role');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 15. getSubagentStatus returns status from spec (idle, active, completed, failed)
|
|
186
|
+
test('getSubagentStatus returns status from spec.status field', () => {
|
|
187
|
+
const controller = createAgentSubagentController();
|
|
188
|
+
const subagent = makeSubagent('active-sub');
|
|
189
|
+
subagent.status = { phase: 'active', sessionRef: 'session-xyz' };
|
|
190
|
+
const status = controller.getSubagentStatus(subagent);
|
|
191
|
+
assert.equal(status.phase, 'active', 'should return the phase from status');
|
|
192
|
+
assert.equal(status.sessionRef, 'session-xyz', 'should return sessionRef from status');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// 16. BOUNDARY exported with correct role
|
|
196
|
+
test('AGENT_SUBAGENT_CONTROLLER_BOUNDARY is exported with correct role', () => {
|
|
197
|
+
assert.ok(AGENT_SUBAGENT_CONTROLLER_BOUNDARY, 'BOUNDARY should be exported');
|
|
198
|
+
assert.equal(AGENT_SUBAGENT_CONTROLLER_BOUNDARY.role, 'agent-subagent-controller', 'role should be agent-subagent-controller');
|
|
199
|
+
assert.ok(Array.isArray(AGENT_SUBAGENT_CONTROLLER_BOUNDARY.owns), 'owns should be an array');
|
|
200
|
+
assert.ok(Array.isArray(AGENT_SUBAGENT_CONTROLLER_BOUNDARY.delegatesTo), 'delegatesTo should be an array');
|
|
201
|
+
});
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
createAgentTransportBindingController,
|
|
5
|
+
validateAgentTransportBinding,
|
|
6
|
+
createResource,
|
|
7
|
+
AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY
|
|
8
|
+
} from '../src/index.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Acceptance criteria: Slice 1.2b — Agent Transport Binding Controller
|
|
12
|
+
//
|
|
13
|
+
// An AgentTransportBinding connects an adapter to a specific endpoint.
|
|
14
|
+
// It specifies binding name, adapterRef, connection endpoint, protocol,
|
|
15
|
+
// health status tracking, and a reconnect policy.
|
|
16
|
+
//
|
|
17
|
+
// All tests in this file are expected to FAIL until the controller is
|
|
18
|
+
// implemented and exported from src/index.js.
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const VALID_PROTOCOLS = ['stdio', 'http', 'websocket', 'unix'];
|
|
22
|
+
|
|
23
|
+
function makeBinding(name, overrides = {}) {
|
|
24
|
+
return createResource('AgentTransportBinding', { name, namespace: 'krate-org-default' }, {
|
|
25
|
+
organizationRef: 'default',
|
|
26
|
+
adapterRef: 'claude-code-adapter',
|
|
27
|
+
endpoint: 'http://localhost:8080',
|
|
28
|
+
protocol: 'http',
|
|
29
|
+
...overrides
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// 1. Factory and shape
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
test('createAgentTransportBindingController returns a controller with validate method', () => {
|
|
38
|
+
const controller = createAgentTransportBindingController();
|
|
39
|
+
assert.ok(controller, 'controller must be truthy');
|
|
40
|
+
assert.equal(typeof controller.validate, 'function', 'controller must expose a validate method');
|
|
41
|
+
assert.equal(controller.role, 'agent-transport-binding-controller', 'controller must declare its role');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// 2. validate — happy path
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
test('validate accepts valid binding with name, adapterRef, endpoint, protocol', () => {
|
|
49
|
+
const controller = createAgentTransportBindingController();
|
|
50
|
+
const binding = makeBinding('my-transport-binding');
|
|
51
|
+
const result = controller.validate(binding);
|
|
52
|
+
|
|
53
|
+
assert.equal(result.valid, true, 'valid binding must pass validation');
|
|
54
|
+
assert.ok(Array.isArray(result.errors), 'result must contain an errors array');
|
|
55
|
+
assert.equal(result.errors.length, 0, 'errors array must be empty for a valid binding');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// 3. validate — missing name
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
test('validate rejects missing name', () => {
|
|
63
|
+
const controller = createAgentTransportBindingController();
|
|
64
|
+
const binding = {
|
|
65
|
+
apiVersion: 'krate.a5c.ai/v1alpha1',
|
|
66
|
+
kind: 'AgentTransportBinding',
|
|
67
|
+
metadata: { namespace: 'krate-org-default', labels: {}, annotations: {} },
|
|
68
|
+
spec: {
|
|
69
|
+
organizationRef: 'default',
|
|
70
|
+
adapterRef: 'claude-code-adapter',
|
|
71
|
+
endpoint: 'http://localhost:8080',
|
|
72
|
+
protocol: 'http'
|
|
73
|
+
},
|
|
74
|
+
status: {}
|
|
75
|
+
};
|
|
76
|
+
const result = controller.validate(binding);
|
|
77
|
+
|
|
78
|
+
assert.equal(result.valid, false, 'binding without name must fail validation');
|
|
79
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
80
|
+
assert.ok(
|
|
81
|
+
result.errors.some((e) => /name/i.test(e)),
|
|
82
|
+
'at least one error must mention "name"'
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// 4. validate — missing adapterRef
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
test('validate rejects missing adapterRef', () => {
|
|
91
|
+
const controller = createAgentTransportBindingController();
|
|
92
|
+
const binding = makeBinding('no-adapter-binding');
|
|
93
|
+
delete binding.spec.adapterRef;
|
|
94
|
+
const result = controller.validate(binding);
|
|
95
|
+
|
|
96
|
+
assert.equal(result.valid, false, 'binding without adapterRef must fail validation');
|
|
97
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
98
|
+
assert.ok(
|
|
99
|
+
result.errors.some((e) => /adapterRef/i.test(e)),
|
|
100
|
+
'at least one error must mention "adapterRef"'
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// 5. validate — missing endpoint
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
test('validate rejects missing endpoint', () => {
|
|
109
|
+
const controller = createAgentTransportBindingController();
|
|
110
|
+
const binding = makeBinding('no-endpoint-binding');
|
|
111
|
+
delete binding.spec.endpoint;
|
|
112
|
+
const result = controller.validate(binding);
|
|
113
|
+
|
|
114
|
+
assert.equal(result.valid, false, 'binding without endpoint must fail validation');
|
|
115
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
116
|
+
assert.ok(
|
|
117
|
+
result.errors.some((e) => /endpoint/i.test(e)),
|
|
118
|
+
'at least one error must mention "endpoint"'
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// 6. validate — invalid protocol
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
test('validate rejects invalid protocol (not in stdio/http/websocket/unix)', () => {
|
|
127
|
+
const controller = createAgentTransportBindingController();
|
|
128
|
+
const binding = makeBinding('bad-protocol-binding', { protocol: 'grpc' });
|
|
129
|
+
const result = controller.validate(binding);
|
|
130
|
+
|
|
131
|
+
assert.equal(result.valid, false, 'binding with unsupported protocol must fail validation');
|
|
132
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
133
|
+
assert.ok(
|
|
134
|
+
result.errors.some((e) => /protocol/i.test(e)),
|
|
135
|
+
'at least one error must mention "protocol"'
|
|
136
|
+
);
|
|
137
|
+
assert.ok(
|
|
138
|
+
result.errors.some((e) => VALID_PROTOCOLS.some((p) => e.includes(p))),
|
|
139
|
+
'error must enumerate valid protocols'
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 7. getConnectionStatus — default 'unknown' for new binding
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
test('getConnectionStatus returns default "unknown" for new binding', () => {
|
|
148
|
+
const controller = createAgentTransportBindingController();
|
|
149
|
+
const binding = makeBinding('new-binding');
|
|
150
|
+
const status = controller.getConnectionStatus(binding);
|
|
151
|
+
|
|
152
|
+
assert.ok(status, 'getConnectionStatus must return a value');
|
|
153
|
+
assert.equal(status.connectionStatus, 'unknown', 'default connection status must be "unknown"');
|
|
154
|
+
assert.equal(status.bindingName, binding.metadata.name, 'result must carry the binding name');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// 8. getReconnectPolicy — returns policy from spec with defaults
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
test('getReconnectPolicy returns policy from spec with defaults', () => {
|
|
162
|
+
const controller = createAgentTransportBindingController();
|
|
163
|
+
const binding = makeBinding('policy-binding', {
|
|
164
|
+
reconnectPolicy: {
|
|
165
|
+
maxRetries: 5,
|
|
166
|
+
backoffMs: 500,
|
|
167
|
+
maxBackoffMs: 30000
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const policy = controller.getReconnectPolicy(binding);
|
|
171
|
+
|
|
172
|
+
assert.ok(policy, 'getReconnectPolicy must return a value');
|
|
173
|
+
assert.equal(policy.maxRetries, 5, 'maxRetries must match spec');
|
|
174
|
+
assert.equal(policy.backoffMs, 500, 'backoffMs must match spec');
|
|
175
|
+
assert.equal(policy.maxBackoffMs, 30000, 'maxBackoffMs must match spec');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// 9. getReconnectPolicy — returns defaults when no policy in spec
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
test('getReconnectPolicy returns defaults when no policy in spec', () => {
|
|
183
|
+
const controller = createAgentTransportBindingController();
|
|
184
|
+
const binding = makeBinding('no-policy-binding');
|
|
185
|
+
// no reconnectPolicy in spec
|
|
186
|
+
const policy = controller.getReconnectPolicy(binding);
|
|
187
|
+
|
|
188
|
+
assert.ok(policy, 'getReconnectPolicy must return a value');
|
|
189
|
+
assert.ok(typeof policy.maxRetries === 'number', 'maxRetries must default to a number');
|
|
190
|
+
assert.ok(typeof policy.backoffMs === 'number', 'backoffMs must default to a number');
|
|
191
|
+
assert.ok(typeof policy.maxBackoffMs === 'number', 'maxBackoffMs must default to a number');
|
|
192
|
+
assert.ok(policy.maxRetries >= 0, 'maxRetries default must be non-negative');
|
|
193
|
+
assert.ok(policy.backoffMs >= 0, 'backoffMs default must be non-negative');
|
|
194
|
+
assert.ok(policy.maxBackoffMs >= policy.backoffMs, 'maxBackoffMs must be >= backoffMs');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// 10. validate — rejects null resource
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
test('validate rejects null resource', () => {
|
|
202
|
+
const controller = createAgentTransportBindingController();
|
|
203
|
+
const result = controller.validate(null);
|
|
204
|
+
|
|
205
|
+
assert.equal(result.valid, false, 'null resource must fail validation');
|
|
206
|
+
assert.ok(result.errors.length > 0, 'errors array must not be empty');
|
|
207
|
+
assert.ok(
|
|
208
|
+
result.errors.some((e) => /null|undefined/i.test(e)),
|
|
209
|
+
'error must mention null or undefined'
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// 11. BOUNDARY object exported with correct role
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
test('AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY is exported and has correct role', () => {
|
|
218
|
+
assert.ok(AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY, 'BOUNDARY must be exported');
|
|
219
|
+
assert.equal(
|
|
220
|
+
AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY.role,
|
|
221
|
+
'agent-transport-binding-controller',
|
|
222
|
+
'BOUNDARY role must be "agent-transport-binding-controller"'
|
|
223
|
+
);
|
|
224
|
+
assert.ok(
|
|
225
|
+
Array.isArray(AGENT_TRANSPORT_BINDING_CONTROLLER_BOUNDARY.owns),
|
|
226
|
+
'BOUNDARY must declare owned concerns'
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// 12. validate — all valid protocols are accepted
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
test('validate accepts each valid protocol (stdio, http, websocket, unix)', () => {
|
|
235
|
+
const controller = createAgentTransportBindingController();
|
|
236
|
+
for (const protocol of VALID_PROTOCOLS) {
|
|
237
|
+
const binding = makeBinding(`binding-${protocol}`, { protocol });
|
|
238
|
+
const result = controller.validate(binding);
|
|
239
|
+
assert.equal(result.valid, true, `protocol "${protocol}" must be accepted`);
|
|
240
|
+
assert.equal(result.errors.length, 0, `no errors expected for protocol "${protocol}"`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// 13. getConnectionStatus — reads from status.connectionStatus when present
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
test('getConnectionStatus reads connectionStatus from resource status field when present', () => {
|
|
249
|
+
const controller = createAgentTransportBindingController();
|
|
250
|
+
const binding = makeBinding('connected-binding');
|
|
251
|
+
binding.status = { connectionStatus: 'connected' };
|
|
252
|
+
const status = controller.getConnectionStatus(binding);
|
|
253
|
+
|
|
254
|
+
assert.equal(status.connectionStatus, 'connected', 'must reflect status from resource status field');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// 14. validate accumulates errors for multiple missing fields
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
test('validate accumulates errors when both adapterRef and endpoint are missing', () => {
|
|
262
|
+
const controller = createAgentTransportBindingController();
|
|
263
|
+
const binding = makeBinding('double-missing-binding');
|
|
264
|
+
delete binding.spec.adapterRef;
|
|
265
|
+
delete binding.spec.endpoint;
|
|
266
|
+
const result = controller.validate(binding);
|
|
267
|
+
|
|
268
|
+
assert.equal(result.valid, false, 'binding with multiple missing fields must fail validation');
|
|
269
|
+
assert.ok(
|
|
270
|
+
result.errors.some((e) => /adapterRef/i.test(e)),
|
|
271
|
+
'errors must include adapterRef error'
|
|
272
|
+
);
|
|
273
|
+
assert.ok(
|
|
274
|
+
result.errors.some((e) => /endpoint/i.test(e)),
|
|
275
|
+
'errors must include endpoint error'
|
|
276
|
+
);
|
|
277
|
+
assert.ok(result.errors.length >= 2, 'must accumulate at least two errors');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
// 15. validateAgentTransportBinding standalone export
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
test('validateAgentTransportBinding is exported and validates correctly', () => {
|
|
285
|
+
assert.equal(typeof validateAgentTransportBinding, 'function', 'validateAgentTransportBinding must be a named export');
|
|
286
|
+
|
|
287
|
+
const binding = makeBinding('standalone-validate-binding');
|
|
288
|
+
const result = validateAgentTransportBinding(binding);
|
|
289
|
+
|
|
290
|
+
assert.ok(result, 'validateAgentTransportBinding must return a result');
|
|
291
|
+
assert.ok('valid' in result, 'result must have a valid property');
|
|
292
|
+
assert.ok(Array.isArray(result.errors), 'result must have an errors array');
|
|
293
|
+
assert.equal(result.valid, true, 'a fully-specified binding must pass standalone validation');
|
|
294
|
+
});
|