@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,470 @@
1
+ /**
2
+ * External Integration Tests — Wire external controllers into runtime
3
+ *
4
+ * Tests that the api-controller properly delegates to sync-controller,
5
+ * write-controller, conflict-controller, and webhook-controller.
6
+ */
7
+ import assert from 'node:assert/strict';
8
+ import test from 'node:test';
9
+ import { createKrateApiController } from '../src/api-controller.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /** Minimal resource gateway stub for testing */
16
+ function makeGateway(overrides = {}) {
17
+ const store = new Map();
18
+ return {
19
+ namespace: 'krate-system',
20
+ resourceDefinitions: {},
21
+ async snapshot() { return { resources: {}, namespace: 'krate-system' }; },
22
+ async list(kind) { return { items: [] }; },
23
+ async get(kind, name) { return null; },
24
+ async apply(resource) {
25
+ store.set(resource.metadata?.name, resource);
26
+ return { operation: 'apply', resource };
27
+ },
28
+ async delete(kind, name) { return { operation: 'delete' }; },
29
+ watch() { return { close: () => {} }; },
30
+ ...overrides
31
+ };
32
+ }
33
+
34
+ function makeController(gatewayOverrides = {}) {
35
+ return createKrateApiController({ resourceGateway: makeGateway(gatewayOverrides) });
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Test 1: syncExternalBinding exists on controller
40
+ // ---------------------------------------------------------------------------
41
+
42
+ test('api-controller exposes syncExternalBinding method', () => {
43
+ const controller = makeController();
44
+ assert.equal(typeof controller.syncExternalBinding, 'function',
45
+ 'controller must expose syncExternalBinding');
46
+ });
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Test 2: syncExternalBinding creates sync controller and runs sync
50
+ // ---------------------------------------------------------------------------
51
+
52
+ test('syncExternalBinding creates sync controller and upserts resource', async () => {
53
+ const controller = makeController();
54
+
55
+ const result = await controller.syncExternalBinding('github-binding', {
56
+ kind: 'Repository',
57
+ localName: 'my-repo',
58
+ namespace: 'default',
59
+ spec: { organizationRef: 'default' },
60
+ externalEnvelope: {
61
+ nativeId: 'github-repo-42',
62
+ url: 'https://github.com/org/repo',
63
+ etag: '"abc"',
64
+ providerRef: 'github-provider'
65
+ }
66
+ });
67
+
68
+ assert.ok(result, 'syncExternalBinding must return a result');
69
+ assert.ok(result.resource, 'result must include the upserted resource');
70
+ assert.equal(result.resource.status?.external?.nativeId, 'github-repo-42',
71
+ 'upserted resource must have correct nativeId');
72
+ });
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Test 3: syncExternalBinding calls persistFn (applyResource) for watermark
76
+ // ---------------------------------------------------------------------------
77
+
78
+ test('syncExternalBinding with watermark timestamp persists watermark via applyResource', async () => {
79
+ const applied = [];
80
+ const controller = createKrateApiController({
81
+ resourceGateway: makeGateway({
82
+ async apply(resource) {
83
+ applied.push(JSON.parse(JSON.stringify(resource)));
84
+ return { operation: 'apply', resource };
85
+ }
86
+ })
87
+ });
88
+
89
+ await controller.syncExternalBinding('github-binding-wm', {
90
+ kind: 'Repository',
91
+ localName: 'wm-repo',
92
+ namespace: 'default',
93
+ spec: {},
94
+ externalEnvelope: { nativeId: 'id-1', url: 'https://example.com', etag: '"v1"', providerRef: 'github' },
95
+ watermark: '2024-01-01T10:00:00.000Z'
96
+ });
97
+
98
+ // Allow async persist tick
99
+ await new Promise((r) => setImmediate(r));
100
+
101
+ assert.ok(applied.length >= 1, 'applyResource must be called at least once for the upserted resource');
102
+ const kinds = applied.map((r) => r.kind);
103
+ assert.ok(kinds.some((k) => k), 'applied resources must have kind fields');
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Test 4: writeController called from api-controller creates and persists intent
108
+ // ---------------------------------------------------------------------------
109
+
110
+ test('api-controller exposes approveExternalWriteIntent method', () => {
111
+ const controller = makeController();
112
+ assert.equal(typeof controller.approveExternalWriteIntent, 'function',
113
+ 'controller must expose approveExternalWriteIntent');
114
+ });
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Test 5: createExternalWriteIntent creates a WriteIntent via write controller
118
+ // ---------------------------------------------------------------------------
119
+
120
+ test('api-controller exposes createExternalWriteIntent method', () => {
121
+ const controller = makeController();
122
+ assert.equal(typeof controller.createExternalWriteIntent, 'function',
123
+ 'controller must expose createExternalWriteIntent');
124
+ });
125
+
126
+ test('createExternalWriteIntent creates intent with correct phase', async () => {
127
+ const controller = makeController();
128
+ const result = await controller.createExternalWriteIntent({
129
+ interfaceKey: 'issueTracking',
130
+ operation: 'createIssue',
131
+ payload: { title: 'Bug fix' },
132
+ resourceRef: 'org/repo#1',
133
+ requiresApproval: false
134
+ });
135
+
136
+ assert.ok(result, 'createExternalWriteIntent must return a result');
137
+ assert.ok(result.intent, 'result must include the intent');
138
+ assert.equal(result.intent.status.phase, 'ReadyToSend',
139
+ 'intent without approval must start as ReadyToSend');
140
+ });
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Test 6: conflictController detects divergence when sync finds changed resource
144
+ // ---------------------------------------------------------------------------
145
+
146
+ test('api-controller exposes detectExternalConflict method', () => {
147
+ const controller = makeController();
148
+ assert.equal(typeof controller.detectExternalConflict, 'function',
149
+ 'controller must expose detectExternalConflict');
150
+ });
151
+
152
+ test('detectExternalConflict creates conflict when local and external values differ', async () => {
153
+ const controller = makeController();
154
+ const result = await controller.detectExternalConflict({
155
+ resourceRef: 'org/repo#issue-1',
156
+ fieldPath: 'spec.title',
157
+ localValue: 'Local Title',
158
+ externalValue: 'External Title'
159
+ });
160
+
161
+ assert.ok(result, 'detectExternalConflict must return a result');
162
+ assert.ok(result.conflict, 'result must include a conflict resource when values differ');
163
+ assert.equal(result.conflict.status.phase, 'Open',
164
+ 'detected conflict must start as Open');
165
+ });
166
+
167
+ test('detectExternalConflict returns null conflict when values match', async () => {
168
+ const controller = makeController();
169
+ const result = await controller.detectExternalConflict({
170
+ resourceRef: 'org/repo#issue-2',
171
+ fieldPath: 'spec.title',
172
+ localValue: 'Same Value',
173
+ externalValue: 'Same Value'
174
+ });
175
+
176
+ assert.ok(result, 'detectExternalConflict must return a result');
177
+ assert.equal(result.conflict, null,
178
+ 'conflict must be null when values are equal');
179
+ });
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Test 7: Full flow — webhook → normalize → sync → detect conflict → resolve
183
+ // ---------------------------------------------------------------------------
184
+
185
+ test('api-controller exposes processExternalWebhook method', () => {
186
+ const controller = makeController();
187
+ assert.equal(typeof controller.processExternalWebhook, 'function',
188
+ 'controller must expose processExternalWebhook');
189
+ });
190
+
191
+ test('processExternalWebhook processes delivery and emits to subscribers', async () => {
192
+ const controller = makeController();
193
+ const eventsReceived = [];
194
+
195
+ const result = await controller.processExternalWebhook({
196
+ deliveryId: 'delivery-001',
197
+ eventType: 'pull_request',
198
+ payload: { action: 'opened', repo: 'org/repo' },
199
+ rawBody: '{"action":"opened","repo":"org/repo"}',
200
+ providerType: 'github',
201
+ secret: 'my-secret'
202
+ });
203
+
204
+ assert.ok(result, 'processExternalWebhook must return a result');
205
+ assert.ok(!result.error, `must not error: ${result.message}`);
206
+ assert.ok('duplicate' in result || 'deliveryId' in result || 'queued' in result,
207
+ 'result must have delivery tracking fields');
208
+ });
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Test 8: api-controller.syncExternalBinding creates sync controller and runs sync
212
+ // ---------------------------------------------------------------------------
213
+
214
+ test('syncExternalBinding returns sync state for the binding', async () => {
215
+ const controller = makeController();
216
+
217
+ // Sync a resource
218
+ await controller.syncExternalBinding('binding-status-test', {
219
+ kind: 'Repository',
220
+ localName: 'status-repo',
221
+ namespace: 'default',
222
+ spec: { organizationRef: 'default' },
223
+ externalEnvelope: {
224
+ nativeId: 'id-status-1',
225
+ url: 'https://github.com/org/status-repo',
226
+ etag: '"v1"',
227
+ providerRef: 'github'
228
+ }
229
+ });
230
+
231
+ assert.ok(true, 'syncExternalBinding must complete without error');
232
+ });
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Test 9: api-controller.resolveExternalConflict calls conflict controller
236
+ // ---------------------------------------------------------------------------
237
+
238
+ test('api-controller exposes resolveExternalConflict method', () => {
239
+ const controller = makeController();
240
+ assert.equal(typeof controller.resolveExternalConflict, 'function',
241
+ 'controller must expose resolveExternalConflict');
242
+ });
243
+
244
+ test('resolveExternalConflict resolves an existing Open conflict', async () => {
245
+ const controller = makeController();
246
+
247
+ // First detect a conflict
248
+ const detectResult = await controller.detectExternalConflict({
249
+ resourceRef: 'org/repo#3',
250
+ fieldPath: 'spec.body',
251
+ localValue: 'Old',
252
+ externalValue: 'New'
253
+ });
254
+ assert.ok(detectResult.conflict, 'must detect conflict first');
255
+
256
+ // Now resolve it using the prefer-external strategy
257
+ const resolveResult = await controller.resolveExternalConflict({
258
+ conflictName: detectResult.conflict.metadata.name,
259
+ strategy: 'prefer-external',
260
+ resources: { ExternalSyncConflict: [detectResult.conflict] }
261
+ });
262
+
263
+ assert.ok(resolveResult, 'resolveExternalConflict must return a result');
264
+ assert.ok(!resolveResult.error, `must not error: ${resolveResult.message}`);
265
+ assert.equal(resolveResult.conflict.status.phase, 'Resolved',
266
+ 'conflict must be Resolved after resolution');
267
+ assert.equal(resolveResult.resolution.chosenValue, 'New',
268
+ 'prefer-external must choose the external value');
269
+ });
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Test 10: api-controller.approveExternalWriteIntent calls write controller
273
+ // ---------------------------------------------------------------------------
274
+
275
+ test('approveExternalWriteIntent approves a PendingApproval intent', async () => {
276
+ const controller = makeController();
277
+
278
+ // Create a pending approval intent
279
+ const createResult = await controller.createExternalWriteIntent({
280
+ interfaceKey: 'gitForge',
281
+ operation: 'createPR',
282
+ payload: { title: 'My PR' },
283
+ resourceRef: 'org/repo#pr-1',
284
+ requiresApproval: true
285
+ });
286
+ assert.ok(createResult.intent, 'must create intent');
287
+ assert.equal(createResult.intent.status.phase, 'PendingApproval', 'must start as PendingApproval');
288
+
289
+ // Now approve it
290
+ const approveResult = await controller.approveExternalWriteIntent({
291
+ intentName: createResult.intent.metadata.name,
292
+ approvedBy: 'admin-user',
293
+ resources: { ExternalWriteIntent: [createResult.intent] }
294
+ });
295
+
296
+ assert.ok(approveResult, 'approveExternalWriteIntent must return a result');
297
+ assert.ok(!approveResult.error, `must not error: ${approveResult.message}`);
298
+ assert.equal(approveResult.intent.status.phase, 'ReadyToSend',
299
+ 'approved intent must transition to ReadyToSend');
300
+ assert.equal(approveResult.intent.status.approvedBy, 'admin-user',
301
+ 'approver must be recorded');
302
+ });
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Test 11: api-controller.cancelExternalWriteIntent cancels an intent
306
+ // ---------------------------------------------------------------------------
307
+
308
+ test('api-controller exposes cancelExternalWriteIntent method', () => {
309
+ const controller = makeController();
310
+ assert.equal(typeof controller.cancelExternalWriteIntent, 'function',
311
+ 'controller must expose cancelExternalWriteIntent');
312
+ });
313
+
314
+ test('cancelExternalWriteIntent rejects a PendingApproval intent', async () => {
315
+ const controller = makeController();
316
+
317
+ const createResult = await controller.createExternalWriteIntent({
318
+ interfaceKey: 'cicd',
319
+ operation: 'triggerBuild',
320
+ payload: { branch: 'main' },
321
+ resourceRef: 'org/repo#build-1',
322
+ requiresApproval: true
323
+ });
324
+ assert.ok(createResult.intent, 'must create intent');
325
+
326
+ const cancelResult = await controller.cancelExternalWriteIntent({
327
+ intentName: createResult.intent.metadata.name,
328
+ cancelledBy: 'user-1',
329
+ resources: { ExternalWriteIntent: [createResult.intent] }
330
+ });
331
+
332
+ assert.ok(cancelResult, 'cancelExternalWriteIntent must return a result');
333
+ assert.ok(!cancelResult.error, `must not error: ${cancelResult.message}`);
334
+ assert.equal(cancelResult.intent.status.phase, 'Rejected',
335
+ 'cancelled intent must be Rejected');
336
+ });
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Test 12: api-controller.getExternalSyncStatus returns binding sync state
340
+ // ---------------------------------------------------------------------------
341
+
342
+ test('api-controller exposes getExternalSyncStatus method', () => {
343
+ const controller = makeController();
344
+ assert.equal(typeof controller.getExternalSyncStatus, 'function',
345
+ 'controller must expose getExternalSyncStatus');
346
+ });
347
+
348
+ test('getExternalSyncStatus returns null watermark for unknown binding', async () => {
349
+ const controller = makeController();
350
+ const result = await controller.getExternalSyncStatus('no-such-binding');
351
+
352
+ assert.ok(result, 'getExternalSyncStatus must return a result');
353
+ assert.equal(result.watermark, null,
354
+ 'watermark must be null for unseen binding');
355
+ });
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // Test 13: processWebhookAndSync — webhook delivery → event normalization → sync controller upserts resource
359
+ // ---------------------------------------------------------------------------
360
+
361
+ test('syncExternalBinding normalizes the provided envelope into a K8s-style resource', async () => {
362
+ const controller = makeController();
363
+ const result = await controller.syncExternalBinding('github-binding-norm', {
364
+ kind: 'PullRequest',
365
+ localName: 'pr-101',
366
+ namespace: 'krate-org-acme',
367
+ spec: { organizationRef: 'acme', title: 'Fix bug' },
368
+ externalEnvelope: {
369
+ nativeId: 'pr-github-101',
370
+ url: 'https://github.com/acme/repo/pull/101',
371
+ etag: '"pr-etag-1"',
372
+ providerRef: 'github'
373
+ }
374
+ });
375
+
376
+ assert.ok(result.resource, 'result must include resource');
377
+ assert.equal(result.resource.kind, 'PullRequest', 'resource kind must match input');
378
+ assert.equal(result.resource.metadata.name, 'pr-101', 'resource name must match localName');
379
+ assert.equal(result.resource.metadata.namespace, 'krate-org-acme', 'namespace must be preserved');
380
+ assert.equal(result.resource.status.external.nativeId, 'pr-github-101', 'nativeId must be set');
381
+ });
382
+
383
+ // ---------------------------------------------------------------------------
384
+ // Test 14: syncController called with persistFn persists watermark and resource
385
+ // ---------------------------------------------------------------------------
386
+
387
+ test('syncExternalBinding with watermark option advances the watermark', async () => {
388
+ const applied = [];
389
+ const controller = createKrateApiController({
390
+ resourceGateway: makeGateway({
391
+ async apply(resource) {
392
+ applied.push(JSON.parse(JSON.stringify(resource)));
393
+ return { operation: 'apply', resource };
394
+ }
395
+ })
396
+ });
397
+
398
+ const watermarkTs = '2025-03-15T09:30:00.000Z';
399
+ await controller.syncExternalBinding('wm-binding-adv', {
400
+ kind: 'Repository',
401
+ localName: 'adv-repo',
402
+ namespace: 'default',
403
+ spec: {},
404
+ externalEnvelope: { nativeId: 'id-wm', url: 'https://x.com', etag: '"wm"', providerRef: 'github' },
405
+ watermark: watermarkTs
406
+ });
407
+
408
+ await new Promise((r) => setImmediate(r));
409
+
410
+ // At least the upserted resource must have been applied
411
+ assert.ok(applied.length >= 1, 'at least one resource must be applied');
412
+
413
+ // Look for the watermark in the applied resources payload
414
+ const applied2 = await controller.getExternalSyncStatus('wm-binding-adv');
415
+ assert.equal(applied2.watermark, watermarkTs,
416
+ 'getExternalSyncStatus must reflect the advanced watermark');
417
+ });
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Test 15: Full end-to-end flow: webhook → normalize → sync → detect conflict → resolve
421
+ // ---------------------------------------------------------------------------
422
+
423
+ test('end-to-end: processExternalWebhook → syncExternalBinding → detectExternalConflict → resolveExternalConflict', async () => {
424
+ const controller = makeController();
425
+
426
+ // Step 1: Process an inbound webhook
427
+ const webhookResult = await controller.processExternalWebhook({
428
+ deliveryId: 'e2e-delivery-001',
429
+ eventType: 'issues',
430
+ payload: { action: 'edited', issue: { id: 5001, title: 'New Title' } },
431
+ rawBody: '{"action":"edited","issue":{"id":5001,"title":"New Title"}}',
432
+ providerType: 'github',
433
+ secret: 'e2e-secret'
434
+ });
435
+ assert.ok(webhookResult, 'processExternalWebhook must succeed');
436
+
437
+ // Step 2: Sync the resource from the external provider
438
+ const syncResult = await controller.syncExternalBinding('e2e-github-binding', {
439
+ kind: 'Issue',
440
+ localName: 'issue-5001',
441
+ namespace: 'default',
442
+ spec: { title: 'New Title', organizationRef: 'default' },
443
+ externalEnvelope: {
444
+ nativeId: 'gh-issue-5001',
445
+ url: 'https://github.com/org/repo/issues/5001',
446
+ etag: '"etag-v2"',
447
+ providerRef: 'github'
448
+ }
449
+ });
450
+ assert.ok(syncResult.resource, 'syncExternalBinding must return a resource');
451
+
452
+ // Step 3: Detect a conflict between local and external state
453
+ const conflictResult = await controller.detectExternalConflict({
454
+ resourceRef: 'org/repo#issue-5001',
455
+ fieldPath: 'spec.title',
456
+ localValue: 'Old Title',
457
+ externalValue: 'New Title'
458
+ });
459
+ assert.ok(conflictResult.conflict, 'conflict must be detected when titles differ');
460
+
461
+ // Step 4: Resolve the conflict
462
+ const resolveResult = await controller.resolveExternalConflict({
463
+ conflictName: conflictResult.conflict.metadata.name,
464
+ strategy: 'prefer-external',
465
+ resources: { ExternalSyncConflict: [conflictResult.conflict] }
466
+ });
467
+ assert.ok(!resolveResult.error, `conflict resolution must succeed: ${resolveResult.message}`);
468
+ assert.equal(resolveResult.conflict.status.phase, 'Resolved',
469
+ 'conflict must be Resolved after end-to-end flow');
470
+ });