@a5c-ai/krate 5.0.1-staging.00fa5317c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-summary.json +724 -0
  9. package/docs/README.md +61 -0
  10. package/docs/agents/README.md +83 -0
  11. package/docs/agents/acceptance-test-matrix.md +193 -0
  12. package/docs/agents/agent-mux-adapter-contract.md +167 -0
  13. package/docs/agents/agent-mux-source-map.md +310 -0
  14. package/docs/agents/agent-run-memory-import-spec.md +256 -0
  15. package/docs/agents/agent-stack-management-spec.md +421 -0
  16. package/docs/agents/api-contract-spec.md +309 -0
  17. package/docs/agents/artifacts-writeback-spec.md +145 -0
  18. package/docs/agents/chart-packaging-spec.md +128 -0
  19. package/docs/agents/ci-orchestration-spec.md +140 -0
  20. package/docs/agents/context-assembly-spec.md +219 -0
  21. package/docs/agents/controller-reconciliation-spec.md +255 -0
  22. package/docs/agents/crd-schema-spec.md +315 -0
  23. package/docs/agents/decision-log-open-questions.md +169 -0
  24. package/docs/agents/developer-implementation-checklist.md +329 -0
  25. package/docs/agents/dispatching-design.md +262 -0
  26. package/docs/agents/gaps-agent-mux-to-krate-crds.md +298 -0
  27. package/docs/agents/glossary.md +66 -0
  28. package/docs/agents/implementation-blueprint.md +324 -0
  29. package/docs/agents/implementation-rollout-slices.md +251 -0
  30. package/docs/agents/memory-context-integration-spec.md +194 -0
  31. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  32. package/docs/agents/memory-operations-runbook.md +121 -0
  33. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  34. package/docs/agents/observability-audit-spec.md +265 -0
  35. package/docs/agents/operator-runbook.md +174 -0
  36. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  37. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  38. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  39. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  40. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  41. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  42. package/docs/agents/org-route-resource-model-spec.md +183 -0
  43. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  44. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  45. package/docs/agents/repository-page-integration-spec.md +255 -0
  46. package/docs/agents/resource-contract-examples.md +808 -0
  47. package/docs/agents/resource-relationship-map.md +190 -0
  48. package/docs/agents/security-threat-model.md +188 -0
  49. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  50. package/docs/agents/storage-migration-spec.md +168 -0
  51. package/docs/agents/subagent-orchestration-spec.md +152 -0
  52. package/docs/agents/system-overview.md +88 -0
  53. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  54. package/docs/agents/traceability-matrix.md +79 -0
  55. package/docs/agents/ui-flow-spec.md +211 -0
  56. package/docs/agents/ui-ux-system-spec.md +426 -0
  57. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  58. package/docs/architecture-spec.md +78 -0
  59. package/docs/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,365 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ createProviderRegistry,
5
+ validateProviderAdapter,
6
+ validateCapabilityManifest
7
+ } from '../src/external/provider-adapter.js';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Acceptance criteria: Slice 3.2 — External Provider Adapter Interface
11
+ //
12
+ // Defines the ExternalProviderAdapter contract and ProviderRegistry that each
13
+ // provider (GitHub, GitLab, etc.) must implement.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers — build minimal valid adapters
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function makeDescriptor(overrides = {}) {
21
+ return {
22
+ providerType: 'github',
23
+ displayName: 'GitHub',
24
+ hosting: ['cloud', 'self-hosted'],
25
+ authModes: ['oauth2', 'pat'],
26
+ apiCapabilities: ['issues', 'pullRequests', 'pipelines'],
27
+ ...overrides
28
+ };
29
+ }
30
+
31
+ function makeAdapter(overrides = {}) {
32
+ return {
33
+ descriptor() {
34
+ return makeDescriptor();
35
+ },
36
+ health() {
37
+ return { status: 'healthy', message: 'All systems operational' };
38
+ },
39
+ issueTracking: {
40
+ listIssues: async () => [],
41
+ createIssue: async () => ({ id: '1' })
42
+ },
43
+ normalizeWebhook(payload) {
44
+ return [{ type: 'push', payload }];
45
+ },
46
+ verifyWebhook(request) {
47
+ return { valid: true, reason: null };
48
+ },
49
+ ...overrides
50
+ };
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // 1. createProviderRegistry — factory shape
55
+ // ---------------------------------------------------------------------------
56
+
57
+ test('createProviderRegistry returns registry with register, get, list methods', () => {
58
+ const registry = createProviderRegistry();
59
+ assert.ok(registry, 'registry must be truthy');
60
+ assert.equal(typeof registry.register, 'function', 'registry must expose register');
61
+ assert.equal(typeof registry.get, 'function', 'registry must expose get');
62
+ assert.equal(typeof registry.list, 'function', 'registry must expose list');
63
+ });
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // 2. register — adds adapter
67
+ // ---------------------------------------------------------------------------
68
+
69
+ test('register adds a provider adapter to the registry', () => {
70
+ const registry = createProviderRegistry();
71
+ const adapter = makeAdapter();
72
+ registry.register('github', adapter);
73
+ const retrieved = registry.get('github');
74
+ assert.ok(retrieved, 'adapter must be retrievable after registration');
75
+ assert.equal(retrieved, adapter, 'retrieved adapter must be the same object');
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 3. get — returns registered adapter
80
+ // ---------------------------------------------------------------------------
81
+
82
+ test('get returns registered adapter by type', () => {
83
+ const registry = createProviderRegistry();
84
+ const adapter = makeAdapter();
85
+ registry.register('gitlab', adapter);
86
+ const retrieved = registry.get('gitlab');
87
+ assert.ok(retrieved, 'get must return the adapter for registered type');
88
+ assert.equal(typeof retrieved.descriptor, 'function', 'adapter must have descriptor method');
89
+ });
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // 4. get — returns null for unregistered type
93
+ // ---------------------------------------------------------------------------
94
+
95
+ test('get returns null for unregistered type', () => {
96
+ const registry = createProviderRegistry();
97
+ const retrieved = registry.get('bitbucket');
98
+ assert.equal(retrieved, null, 'get must return null for unregistered type');
99
+ });
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // 5. list — returns all registered adapter types
103
+ // ---------------------------------------------------------------------------
104
+
105
+ test('list returns all registered adapter types', () => {
106
+ const registry = createProviderRegistry();
107
+ registry.register('github', makeAdapter());
108
+ registry.register('gitlab', makeAdapter());
109
+ registry.register('gitea', makeAdapter());
110
+ const types = registry.list();
111
+ assert.ok(Array.isArray(types), 'list must return an array');
112
+ assert.equal(types.length, 3, 'list must return exactly 3 types');
113
+ assert.ok(types.includes('github'), 'list must include github');
114
+ assert.ok(types.includes('gitlab'), 'list must include gitlab');
115
+ assert.ok(types.includes('gitea'), 'list must include gitea');
116
+ });
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // 6. validateProviderAdapter — accepts valid adapter
120
+ // ---------------------------------------------------------------------------
121
+
122
+ test('validateProviderAdapter accepts valid adapter (descriptor, health, at least one interface)', () => {
123
+ const adapter = makeAdapter();
124
+ const result = validateProviderAdapter(adapter);
125
+ assert.equal(result.valid, true, 'valid adapter must pass validation');
126
+ assert.ok(Array.isArray(result.errors), 'result must have errors array');
127
+ assert.equal(result.errors.length, 0, 'errors array must be empty for valid adapter');
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // 7. validateProviderAdapter — rejects adapter with no interfaces
132
+ // ---------------------------------------------------------------------------
133
+
134
+ test('validateProviderAdapter rejects adapter with no interfaces', () => {
135
+ const adapter = makeAdapter();
136
+ delete adapter.issueTracking;
137
+ // Remove all optional interfaces
138
+ const noInterfaceAdapter = {
139
+ descriptor: adapter.descriptor,
140
+ health: adapter.health,
141
+ normalizeWebhook: adapter.normalizeWebhook,
142
+ verifyWebhook: adapter.verifyWebhook
143
+ };
144
+ const result = validateProviderAdapter(noInterfaceAdapter);
145
+ assert.equal(result.valid, false, 'adapter with no interfaces must fail validation');
146
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
147
+ assert.ok(
148
+ result.errors.some((e) => /interface|issueTracking|cicd|gitForge/i.test(e)),
149
+ 'error must mention missing interfaces'
150
+ );
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // 8. validateProviderAdapter — rejects adapter with missing descriptor
155
+ // ---------------------------------------------------------------------------
156
+
157
+ test('validateProviderAdapter rejects adapter with missing descriptor', () => {
158
+ const adapter = makeAdapter();
159
+ delete adapter.descriptor;
160
+ const result = validateProviderAdapter(adapter);
161
+ assert.equal(result.valid, false, 'adapter without descriptor must fail validation');
162
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
163
+ assert.ok(
164
+ result.errors.some((e) => /descriptor/i.test(e)),
165
+ 'error must mention descriptor'
166
+ );
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // 9. validateProviderAdapter — rejects adapter with missing health method
171
+ // ---------------------------------------------------------------------------
172
+
173
+ test('validateProviderAdapter rejects adapter with missing health method', () => {
174
+ const adapter = makeAdapter();
175
+ delete adapter.health;
176
+ const result = validateProviderAdapter(adapter);
177
+ assert.equal(result.valid, false, 'adapter without health must fail validation');
178
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
179
+ assert.ok(
180
+ result.errors.some((e) => /health/i.test(e)),
181
+ 'error must mention health'
182
+ );
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // 10. descriptor() — shape contract
187
+ // ---------------------------------------------------------------------------
188
+
189
+ test('descriptor() returns provider type, display name, hosting modes, auth modes', () => {
190
+ const adapter = makeAdapter();
191
+ const desc = adapter.descriptor();
192
+ assert.ok(desc, 'descriptor must return a value');
193
+ assert.equal(typeof desc.providerType, 'string', 'providerType must be a string');
194
+ assert.equal(typeof desc.displayName, 'string', 'displayName must be a string');
195
+ assert.ok(Array.isArray(desc.hosting), 'hosting must be an array');
196
+ assert.ok(Array.isArray(desc.authModes), 'authModes must be an array');
197
+ assert.ok(desc.hosting.length > 0, 'hosting must have at least one mode');
198
+ assert.ok(desc.authModes.length > 0, 'authModes must have at least one mode');
199
+ });
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // 11. validateCapabilityManifest — accepts valid manifest
203
+ // ---------------------------------------------------------------------------
204
+
205
+ test('validateCapabilityManifest accepts valid manifest (providerType, interfaces)', () => {
206
+ const manifest = {
207
+ providerType: 'github',
208
+ interfaces: ['issueTracking', 'gitForge']
209
+ };
210
+ const result = validateCapabilityManifest(manifest);
211
+ assert.equal(result.valid, true, 'valid manifest must pass validation');
212
+ assert.ok(Array.isArray(result.errors), 'result must have errors array');
213
+ assert.equal(result.errors.length, 0, 'errors must be empty for valid manifest');
214
+ });
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // 12. validateCapabilityManifest — rejects missing providerType
218
+ // ---------------------------------------------------------------------------
219
+
220
+ test('validateCapabilityManifest rejects missing providerType', () => {
221
+ const manifest = {
222
+ interfaces: ['issueTracking']
223
+ };
224
+ const result = validateCapabilityManifest(manifest);
225
+ assert.equal(result.valid, false, 'manifest without providerType must fail');
226
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
227
+ assert.ok(
228
+ result.errors.some((e) => /providerType/i.test(e)),
229
+ 'error must mention providerType'
230
+ );
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // 13. validateCapabilityManifest — rejects manifest with no supported interfaces
235
+ // ---------------------------------------------------------------------------
236
+
237
+ test('validateCapabilityManifest rejects manifest with no supported interfaces', () => {
238
+ const manifest = {
239
+ providerType: 'github',
240
+ interfaces: []
241
+ };
242
+ const result = validateCapabilityManifest(manifest);
243
+ assert.equal(result.valid, false, 'manifest with empty interfaces must fail');
244
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
245
+ assert.ok(
246
+ result.errors.some((e) => /interface/i.test(e)),
247
+ 'error must mention interfaces'
248
+ );
249
+ });
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // 14. normalizeWebhook — adapter must have normalizeWebhook method
253
+ // ---------------------------------------------------------------------------
254
+
255
+ test('normalizeWebhook contract — adapter must have normalizeWebhook method', () => {
256
+ const adapter = makeAdapter();
257
+ assert.equal(typeof adapter.normalizeWebhook, 'function', 'adapter must have normalizeWebhook');
258
+ const payload = { event: 'push', ref: 'refs/heads/main', repository: { full_name: 'org/repo' } };
259
+ const events = adapter.normalizeWebhook(payload);
260
+ assert.ok(Array.isArray(events), 'normalizeWebhook must return an array');
261
+ assert.ok(events.length > 0, 'normalizeWebhook must return at least one event');
262
+ assert.ok('type' in events[0], 'each normalized event must have a type field');
263
+ });
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // 15. verifyWebhook — adapter must have verifyWebhook method
267
+ // ---------------------------------------------------------------------------
268
+
269
+ test('verifyWebhook contract — adapter must have verifyWebhook method', () => {
270
+ const adapter = makeAdapter();
271
+ assert.equal(typeof adapter.verifyWebhook, 'function', 'adapter must have verifyWebhook');
272
+ const request = { headers: { 'x-hub-signature-256': 'sha256=abc123' }, body: '{}' };
273
+ const result = adapter.verifyWebhook(request);
274
+ assert.ok(result, 'verifyWebhook must return a result');
275
+ assert.ok('valid' in result, 'result must have a valid property');
276
+ assert.equal(typeof result.valid, 'boolean', 'valid must be a boolean');
277
+ });
278
+
279
+ // ---------------------------------------------------------------------------
280
+ // 16. validateProviderAdapter — accepts adapter with cicd interface
281
+ // ---------------------------------------------------------------------------
282
+
283
+ test('validateProviderAdapter accepts adapter with cicd interface instead of issueTracking', () => {
284
+ const adapter = {
285
+ descriptor() { return makeDescriptor(); },
286
+ health() { return { status: 'healthy', message: 'ok' }; },
287
+ cicd: {
288
+ listPipelines: async () => [],
289
+ triggerPipeline: async () => ({ id: 'run-1' })
290
+ },
291
+ normalizeWebhook(payload) { return [{ type: 'pipeline', payload }]; },
292
+ verifyWebhook(request) { return { valid: true, reason: null }; }
293
+ };
294
+ const result = validateProviderAdapter(adapter);
295
+ assert.equal(result.valid, true, 'adapter with cicd interface must pass validation');
296
+ assert.equal(result.errors.length, 0, 'no errors for valid cicd adapter');
297
+ });
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // 17. validateProviderAdapter — accepts adapter with gitForge interface
301
+ // ---------------------------------------------------------------------------
302
+
303
+ test('validateProviderAdapter accepts adapter with gitForge interface instead of issueTracking', () => {
304
+ const adapter = {
305
+ descriptor() { return makeDescriptor(); },
306
+ health() { return { status: 'healthy', message: 'ok' }; },
307
+ gitForge: {
308
+ listRepositories: async () => [],
309
+ createRepository: async () => ({ id: 'repo-1' })
310
+ },
311
+ normalizeWebhook(payload) { return [{ type: 'push', payload }]; },
312
+ verifyWebhook(request) { return { valid: true, reason: null }; }
313
+ };
314
+ const result = validateProviderAdapter(adapter);
315
+ assert.equal(result.valid, true, 'adapter with gitForge interface must pass validation');
316
+ assert.equal(result.errors.length, 0, 'no errors for valid gitForge adapter');
317
+ });
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // 18. health() — shape contract
321
+ // ---------------------------------------------------------------------------
322
+
323
+ test('health() returns status and message with valid status value', () => {
324
+ const adapter = makeAdapter();
325
+ const result = adapter.health();
326
+ assert.ok(result, 'health must return a value');
327
+ assert.ok('status' in result, 'health result must have status');
328
+ assert.ok('message' in result, 'health result must have message');
329
+ const VALID_STATUSES = ['healthy', 'degraded', 'unavailable'];
330
+ assert.ok(VALID_STATUSES.includes(result.status), `status must be one of ${VALID_STATUSES.join(', ')}`);
331
+ });
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // 19. validateCapabilityManifest — rejects invalid interface names
335
+ // ---------------------------------------------------------------------------
336
+
337
+ test('validateCapabilityManifest rejects unknown interface names', () => {
338
+ const manifest = {
339
+ providerType: 'github',
340
+ interfaces: ['unknownInterface']
341
+ };
342
+ const result = validateCapabilityManifest(manifest);
343
+ assert.equal(result.valid, false, 'manifest with unknown interface must fail');
344
+ assert.ok(result.errors.length > 0, 'errors array must not be empty');
345
+ assert.ok(
346
+ result.errors.some((e) => /interface|unknown|invalid/i.test(e)),
347
+ 'error must mention invalid interface'
348
+ );
349
+ });
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // 20. registry — overwriting same type updates the registration
353
+ // ---------------------------------------------------------------------------
354
+
355
+ test('registry allows re-registering a provider type with a new adapter', () => {
356
+ const registry = createProviderRegistry();
357
+ const adapterV1 = makeAdapter();
358
+ const adapterV2 = makeAdapter({ normalizeWebhook: (p) => [{ type: 'v2', payload: p }] });
359
+ registry.register('github', adapterV1);
360
+ registry.register('github', adapterV2);
361
+ const retrieved = registry.get('github');
362
+ assert.equal(retrieved, adapterV2, 'second registration should overwrite the first');
363
+ const types = registry.list();
364
+ assert.equal(types.filter((t) => t === 'github').length, 1, 'github should appear only once in list');
365
+ });
@@ -0,0 +1,215 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ CONFIG_KINDS,
5
+ AGGREGATED_KINDS,
6
+ ALL_KINDS,
7
+ RESOURCE_DEFINITIONS,
8
+ createResource,
9
+ validateResource,
10
+ resourceSchemaForKind,
11
+ storageClassForKind,
12
+ listResourceDefinitions
13
+ } from '../src/resource-model.js';
14
+
15
+ const EXTERNAL_CONFIG_KINDS = [
16
+ 'ExternalBackendProvider',
17
+ 'ExternalBackendBinding',
18
+ 'ExternalBackendSyncPolicy',
19
+ 'ExternalProviderCapabilityManifest'
20
+ ];
21
+
22
+ const EXTERNAL_AGGREGATED_KINDS = [
23
+ 'ExternalWebhookDelivery',
24
+ 'ExternalSyncEvent',
25
+ 'ExternalSyncState',
26
+ 'ExternalWriteIntent',
27
+ 'ExternalSyncConflict',
28
+ 'ExternalObjectLink'
29
+ ];
30
+
31
+ const ALL_EXTERNAL_KINDS = [...EXTERNAL_CONFIG_KINDS, ...EXTERNAL_AGGREGATED_KINDS];
32
+
33
+ /** Minimal valid spec for each external kind, satisfying requiredSpec. */
34
+ function minimalSpecForKind(kind) {
35
+ const specs = {
36
+ ExternalBackendProvider: { organizationRef: 'default', providerType: 'github', endpoint: 'https://api.github.com' },
37
+ ExternalBackendBinding: { organizationRef: 'default', providerRef: 'github-provider', credentialRef: 'github-creds' },
38
+ ExternalBackendSyncPolicy: { organizationRef: 'default', providerRef: 'github-provider', syncInterval: '5m' },
39
+ ExternalProviderCapabilityManifest: { organizationRef: 'default', providerRef: 'github-provider', capabilities: ['webhooks', 'pull-requests'] },
40
+ ExternalWebhookDelivery: { organizationRef: 'default', providerRef: 'github-provider', eventType: 'push', payload: {} },
41
+ ExternalSyncEvent: { organizationRef: 'default', providerRef: 'github-provider', eventKind: 'repository.created', resourceRef: 'repo-1' },
42
+ ExternalSyncState: { organizationRef: 'default', providerRef: 'github-provider', resourceRef: 'repo-1', phase: 'synced' },
43
+ ExternalWriteIntent: { organizationRef: 'default', providerRef: 'github-provider', resourceRef: 'repo-1', operation: 'update' },
44
+ ExternalSyncConflict: { organizationRef: 'default', providerRef: 'github-provider', resourceRef: 'repo-1', conflictKind: 'update-collision' },
45
+ ExternalObjectLink: { organizationRef: 'default', providerRef: 'github-provider', externalId: 'github-repo-12345', localRef: 'repo-1' }
46
+ };
47
+ return specs[kind];
48
+ }
49
+
50
+ describe('external resource config kind membership', () => {
51
+ for (const kind of EXTERNAL_CONFIG_KINDS) {
52
+ it(`resource model includes ${kind} in config kinds`, () => {
53
+ assert.ok(CONFIG_KINDS.has(kind), `${kind} should be in CONFIG_KINDS`);
54
+ });
55
+ }
56
+ });
57
+
58
+ describe('external resource aggregated kind membership', () => {
59
+ for (const kind of EXTERNAL_AGGREGATED_KINDS) {
60
+ it(`resource model includes ${kind} in aggregated kinds`, () => {
61
+ assert.ok(AGGREGATED_KINDS.has(kind), `${kind} should be in AGGREGATED_KINDS`);
62
+ });
63
+ }
64
+ });
65
+
66
+ describe('external resource ALL_KINDS membership', () => {
67
+ for (const kind of ALL_EXTERNAL_KINDS) {
68
+ it(`${kind} is in ALL_KINDS`, () => {
69
+ assert.ok(ALL_KINDS.has(kind), `${kind} should be in ALL_KINDS`);
70
+ });
71
+ }
72
+ });
73
+
74
+ describe('RESOURCE_DEFINITIONS for external kinds', () => {
75
+ for (const kind of ALL_EXTERNAL_KINDS) {
76
+ it(`${kind} has valid definition with storage, context, plural, purpose, requiredSpec`, () => {
77
+ const def = RESOURCE_DEFINITIONS[kind];
78
+ assert.ok(def, `${kind} should exist in RESOURCE_DEFINITIONS`);
79
+ assert.ok(['etcd', 'postgres'].includes(def.storage), `${kind} storage should be etcd or postgres`);
80
+ assert.ok(typeof def.context === 'string' && def.context.length > 0, `${kind} context should be a non-empty string`);
81
+ assert.ok(typeof def.plural === 'string' && def.plural.length > 0, `${kind} plural should be a non-empty string`);
82
+ assert.ok(typeof def.purpose === 'string' && def.purpose.length > 0, `${kind} purpose should be a non-empty string`);
83
+ assert.ok(Array.isArray(def.requiredSpec) && def.requiredSpec.length > 0, `${kind} requiredSpec should be a non-empty array`);
84
+ });
85
+ }
86
+ });
87
+
88
+ describe('createResource for external kinds', () => {
89
+ it("createResource('ExternalBackendProvider', ...) creates valid resource with apiVersion", () => {
90
+ const spec = minimalSpecForKind('ExternalBackendProvider');
91
+ const resource = createResource('ExternalBackendProvider', { name: 'test-provider' }, spec);
92
+ assert.equal(resource.apiVersion, 'krate.a5c.ai/v1alpha1');
93
+ assert.equal(resource.kind, 'ExternalBackendProvider');
94
+ assert.equal(resource.metadata.name, 'test-provider');
95
+ assert.equal(resource.metadata.namespace, 'default');
96
+ assert.ok(resource.metadata.labels !== undefined);
97
+ assert.ok(resource.metadata.annotations !== undefined);
98
+ assert.ok(typeof resource.spec === 'object');
99
+ assert.ok(typeof resource.status === 'object');
100
+ });
101
+
102
+ it("createResource('ExternalBackendBinding', ...) creates valid resource", () => {
103
+ const spec = minimalSpecForKind('ExternalBackendBinding');
104
+ const resource = createResource('ExternalBackendBinding', { name: 'test-binding' }, spec);
105
+ assert.equal(resource.apiVersion, 'krate.a5c.ai/v1alpha1');
106
+ assert.equal(resource.kind, 'ExternalBackendBinding');
107
+ assert.equal(resource.metadata.name, 'test-binding');
108
+ });
109
+
110
+ for (const kind of ALL_EXTERNAL_KINDS) {
111
+ it(`creates a valid ${kind} resource`, () => {
112
+ const spec = minimalSpecForKind(kind);
113
+ const resource = createResource(kind, { name: `test-${kind.toLowerCase()}` }, spec);
114
+ assert.equal(resource.apiVersion, 'krate.a5c.ai/v1alpha1');
115
+ assert.equal(resource.kind, kind);
116
+ assert.equal(resource.metadata.name, `test-${kind.toLowerCase()}`);
117
+ assert.equal(resource.metadata.namespace, 'default');
118
+ assert.ok(typeof resource.spec === 'object');
119
+ assert.ok(typeof resource.status === 'object');
120
+ });
121
+ }
122
+ });
123
+
124
+ describe('All external config kinds have required spec fields (organizationRef)', () => {
125
+ for (const kind of EXTERNAL_CONFIG_KINDS) {
126
+ it(`${kind} has organizationRef in requiredSpec`, () => {
127
+ const def = RESOURCE_DEFINITIONS[kind];
128
+ assert.ok(def, `${kind} should exist in RESOURCE_DEFINITIONS`);
129
+ assert.ok(def.requiredSpec.includes('organizationRef'), `${kind} requiredSpec should include organizationRef`);
130
+ });
131
+ }
132
+ });
133
+
134
+ describe('findResourceDefinition finds external kinds by plural name', () => {
135
+ for (const kind of ALL_EXTERNAL_KINDS) {
136
+ it(`listResourceDefinitions includes ${kind} and its plural name`, () => {
137
+ const defs = listResourceDefinitions();
138
+ const def = defs.find(d => d.kind === kind);
139
+ assert.ok(def, `${kind} should be found in listResourceDefinitions()`);
140
+ assert.ok(typeof def.plural === 'string' && def.plural.length > 0, `${kind} should have a plural name`);
141
+ });
142
+ }
143
+ });
144
+
145
+ describe('external config kinds use etcd storage', () => {
146
+ for (const kind of EXTERNAL_CONFIG_KINDS) {
147
+ it(`${kind} returns etcd`, () => {
148
+ assert.equal(storageClassForKind(kind), 'etcd');
149
+ });
150
+ }
151
+ });
152
+
153
+ describe('external aggregated kinds use postgres storage', () => {
154
+ for (const kind of EXTERNAL_AGGREGATED_KINDS) {
155
+ it(`${kind} returns postgres`, () => {
156
+ assert.equal(storageClassForKind(kind), 'postgres');
157
+ });
158
+ }
159
+ });
160
+
161
+ describe('validateResource rejects missing required spec fields for external kinds', () => {
162
+ for (const kind of ALL_EXTERNAL_KINDS) {
163
+ it(`throws on empty spec for ${kind}`, () => {
164
+ const resource = {
165
+ apiVersion: 'krate.a5c.ai/v1alpha1',
166
+ kind,
167
+ metadata: { name: `invalid-${kind.toLowerCase()}` },
168
+ spec: {},
169
+ status: {}
170
+ };
171
+ const def = RESOURCE_DEFINITIONS[kind];
172
+ assert.throws(
173
+ () => validateResource(resource),
174
+ (err) => {
175
+ assert.ok(err.message.includes(`${kind} spec.${def.requiredSpec[0]} is required`));
176
+ return true;
177
+ }
178
+ );
179
+ });
180
+ }
181
+ });
182
+
183
+ describe('resourceSchemaForKind for external kinds', () => {
184
+ for (const kind of ALL_EXTERNAL_KINDS) {
185
+ it(`returns correct schema for ${kind}`, () => {
186
+ const schema = resourceSchemaForKind(kind);
187
+ assert.equal(schema.apiVersion, 'krate.a5c.ai/v1alpha1');
188
+ assert.equal(schema.kind, kind);
189
+ assert.ok(typeof schema.plural === 'string');
190
+ assert.ok(['etcd', 'postgres'].includes(schema.storage));
191
+ assert.ok(Array.isArray(schema.required.metadata));
192
+ assert.ok(schema.required.metadata.includes('name'));
193
+ assert.ok(Array.isArray(schema.required.spec));
194
+ assert.ok(schema.required.spec.length > 0);
195
+ });
196
+ }
197
+ });
198
+
199
+ describe('kind set counts after external kinds added', () => {
200
+ it('CONFIG_KINDS has 46 members (42 previous + 4 external config)', () => {
201
+ assert.equal(CONFIG_KINDS.size, 46);
202
+ });
203
+
204
+ it('AGGREGATED_KINDS has 29 members (23 previous + 6 external aggregated)', () => {
205
+ assert.equal(AGGREGATED_KINDS.size, 29);
206
+ });
207
+
208
+ it('ALL_KINDS has 75 members (65 previous + 10 external)', () => {
209
+ assert.equal(ALL_KINDS.size, 75);
210
+ });
211
+
212
+ it('listResourceDefinitions returns 75 definitions', () => {
213
+ assert.equal(listResourceDefinitions().length, 75);
214
+ });
215
+ });