@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.
Files changed (118) hide show
  1. package/Dockerfile +31 -29
  2. package/bin/krate-demo.mjs +0 -0
  3. package/bin/krate-server.mjs +0 -0
  4. package/dist/krate-controller-ui.json +808 -10
  5. package/dist/krate-lifecycle.json +1 -1
  6. package/dist/krate-runtime-snapshot.json +223 -53
  7. package/dist/krate-summary.json +40 -3
  8. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  9. package/docs/architecture-v2.md +431 -0
  10. package/docs/openapi.yaml +1275 -0
  11. package/docs/requirements-v2.md +238 -0
  12. package/docs/sdk-api-reference.md +782 -0
  13. package/docs/system-spec-v2.md +352 -0
  14. package/docs/todos.md +4 -0
  15. package/docs/web-console-spec.md +433 -0
  16. package/package.json +1 -1
  17. package/scripts/validate-ui.mjs +305 -207
  18. package/src/agent-adapter-controller.js +169 -0
  19. package/src/agent-approval-controller.js +47 -0
  20. package/src/agent-dispatch-controller.js +130 -7
  21. package/src/agent-gateway-config-controller.js +147 -0
  22. package/src/agent-memory-controller.js +357 -0
  23. package/src/agent-memory-import.js +327 -0
  24. package/src/agent-memory-query.js +292 -0
  25. package/src/agent-memory-repository-source-controller.js +255 -0
  26. package/src/agent-mux-client.js +1 -1
  27. package/src/agent-permission-review.js +102 -14
  28. package/src/agent-project-controller.js +117 -0
  29. package/src/agent-provider-config-controller.js +150 -0
  30. package/src/agent-secret-config-grant-controller.js +282 -0
  31. package/src/agent-session-transcript-controller.js +189 -0
  32. package/src/agent-stack-controller.js +52 -1
  33. package/src/agent-subagent-controller.js +160 -0
  34. package/src/agent-transport-binding-controller.js +121 -0
  35. package/src/agent-trigger-controller.js +273 -0
  36. package/src/agent-workspace-controller.js +702 -0
  37. package/src/agent-writeback-controller.js +302 -0
  38. package/src/api-controller.js +338 -3
  39. package/src/async-controller.js +207 -0
  40. package/src/audit-controller.js +191 -0
  41. package/src/auth.js +48 -6
  42. package/src/controller-client.js +112 -38
  43. package/src/controller-ui.js +96 -16
  44. package/src/data-plane.js +3 -2
  45. package/src/event-bus.js +61 -0
  46. package/src/external/conflict-controller.js +225 -0
  47. package/src/external/github/auth.js +96 -0
  48. package/src/external/github/cicd.js +180 -0
  49. package/src/external/github/git-forge.js +240 -0
  50. package/src/external/github/index.js +144 -0
  51. package/src/external/github/issue-tracking.js +163 -0
  52. package/src/external/provider-adapter.js +161 -0
  53. package/src/external/provider-resource-factory.js +161 -0
  54. package/src/external/sync-controller.js +235 -0
  55. package/src/external/webhook-controller.js +144 -0
  56. package/src/external/write-controller.js +283 -0
  57. package/src/gitea-backend.js +36 -0
  58. package/src/gitea-service.js +173 -0
  59. package/src/http-server.js +226 -0
  60. package/src/index.js +27 -0
  61. package/src/kubernetes-controller-async.js +531 -0
  62. package/src/kubernetes-controller.js +156 -84
  63. package/src/notification-controller.js +178 -0
  64. package/src/org-scoping.js +5 -0
  65. package/src/resource-model.js +26 -8
  66. package/src/runner-controller.js +272 -0
  67. package/src/snapshot-cache.js +157 -0
  68. package/tests/agent-adapter-controller.test.js +361 -0
  69. package/tests/agent-dispatch-controller.test.js +139 -0
  70. package/tests/agent-gateway-config-controller.test.js +386 -0
  71. package/tests/agent-memory-controller.test.js +308 -0
  72. package/tests/agent-memory-import-snapshot.test.js +477 -0
  73. package/tests/agent-memory-query.test.js +404 -0
  74. package/tests/agent-memory-repository-source.test.js +514 -0
  75. package/tests/agent-permission-review-v2.test.js +317 -0
  76. package/tests/agent-project-controller.test.js +302 -0
  77. package/tests/agent-provider-config-controller.test.js +376 -0
  78. package/tests/agent-resources.test.js +35 -19
  79. package/tests/agent-secret-config-grant.test.js +231 -0
  80. package/tests/agent-session-transcript-controller.test.js +499 -0
  81. package/tests/agent-subagent-controller.test.js +201 -0
  82. package/tests/agent-transport-binding-controller.test.js +294 -0
  83. package/tests/agent-trigger-routes.test.js +190 -0
  84. package/tests/agent-trigger-sources.test.js +245 -0
  85. package/tests/agent-workspace-controller.test.js +181 -0
  86. package/tests/agent-writeback.test.js +292 -0
  87. package/tests/approval-persistence.test.js +171 -0
  88. package/tests/async-controller.test.js +252 -0
  89. package/tests/audit-controller.test.js +227 -0
  90. package/tests/codespace-controller.test.js +318 -0
  91. package/tests/controller-client.test.js +133 -0
  92. package/tests/deployment.test.js +43 -29
  93. package/tests/e2e/lifecycle.test.js +5 -2
  94. package/tests/event-bus-integration.test.js +190 -0
  95. package/tests/external-github-forge.test.js +560 -0
  96. package/tests/external-github-issues-cicd.test.js +520 -0
  97. package/tests/external-integration.test.js +470 -0
  98. package/tests/external-persistence.test.js +340 -0
  99. package/tests/external-provider-adapter.test.js +365 -0
  100. package/tests/external-resource-model.test.js +215 -0
  101. package/tests/external-webhook-sync.test.js +287 -0
  102. package/tests/external-write-conflict.test.js +353 -0
  103. package/tests/gitea-service.test.js +253 -0
  104. package/tests/health-check-real.test.js +165 -0
  105. package/tests/integration/full-flow.test.js +266 -0
  106. package/tests/krate.test.js +58 -6
  107. package/tests/memory-search-wiring.test.js +270 -0
  108. package/tests/notification-controller.test.js +196 -0
  109. package/tests/notification-integration.test.js +179 -0
  110. package/tests/org-scoping.test.js +687 -0
  111. package/tests/runner-controller.test.js +327 -0
  112. package/tests/runner-integration.test.js +231 -0
  113. package/tests/session-cookie-hmac.test.js +151 -0
  114. package/tests/snapshot-performance.test.js +315 -0
  115. package/tests/sse-events.test.js +107 -0
  116. package/tests/webhook-trigger.test.js +198 -0
  117. package/tests/workspace-volumes.test.js +312 -0
  118. 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
+ });