@a5c-ai/kradle 5.0.1-staging.3abdf9534c25

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 (295) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +187 -0
  3. package/bin/kradle-demo.mjs +23 -0
  4. package/bin/kradle-server.mjs +14 -0
  5. package/dist/kradle-controller-ui.json +3482 -0
  6. package/dist/kradle-lifecycle.json +201 -0
  7. package/dist/kradle-runtime-snapshot.json +3125 -0
  8. package/dist/kradle-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-kradle-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/architecture-v2.md +2759 -0
  60. package/docs/components/control-plane.md +78 -0
  61. package/docs/components/data-plane.md +69 -0
  62. package/docs/components/hooks-events.md +67 -0
  63. package/docs/components/identity-rbac-policy.md +73 -0
  64. package/docs/components/kubevela-oam.md +70 -0
  65. package/docs/components/operations-publishing.md +81 -0
  66. package/docs/components/runners-ci.md +66 -0
  67. package/docs/components/web-ui.md +94 -0
  68. package/docs/crd-behaviors-and-relationships.md +3926 -0
  69. package/docs/external/README.md +47 -0
  70. package/docs/external/bidirectional-sync-design.md +134 -0
  71. package/docs/external/cicd-interface.md +64 -0
  72. package/docs/external/external-backend-controllers.md +170 -0
  73. package/docs/external/external-backend-crds.md +234 -0
  74. package/docs/external/external-backend-ui-spec.md +151 -0
  75. package/docs/external/external-backend-ux-flows.md +115 -0
  76. package/docs/external/external-object-mapping.md +125 -0
  77. package/docs/external/git-forge-interface.md +68 -0
  78. package/docs/external/github-integration-design.md +151 -0
  79. package/docs/external/issue-tracking-interface.md +66 -0
  80. package/docs/external/provider-capability-manifests.md +204 -0
  81. package/docs/external/provider-catalog.md +139 -0
  82. package/docs/external/provider-rollout-testing.md +78 -0
  83. package/docs/external/research-results.md +48 -0
  84. package/docs/external/security-auth-permissions.md +81 -0
  85. package/docs/external/sync-state-machines.md +108 -0
  86. package/docs/external/unified-external-backend-model.md +107 -0
  87. package/docs/external/user-facing-changes.md +67 -0
  88. package/docs/gaps.md +161 -0
  89. package/docs/install.md +94 -0
  90. package/docs/integration-and-design-decisions.md +1530 -0
  91. package/docs/kradle-design.md +334 -0
  92. package/docs/local-minikube.md +55 -0
  93. package/docs/ontology/README.md +32 -0
  94. package/docs/ontology/bounded-contexts.md +29 -0
  95. package/docs/ontology/events-and-hooks.md +32 -0
  96. package/docs/ontology/oam-kubevela.md +32 -0
  97. package/docs/ontology/operations-and-release.md +25 -0
  98. package/docs/ontology/personas-and-actors.md +32 -0
  99. package/docs/ontology/policies-and-invariants.md +33 -0
  100. package/docs/ontology/problem-space.md +30 -0
  101. package/docs/ontology/resource-contracts.md +40 -0
  102. package/docs/ontology/resource-taxonomy.md +42 -0
  103. package/docs/ontology/runners-and-ci.md +29 -0
  104. package/docs/ontology/solution-space.md +24 -0
  105. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  106. package/docs/ontology/validation-matrix.md +24 -0
  107. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  108. package/docs/ontology/workflows.md +39 -0
  109. package/docs/ontology/world.md +35 -0
  110. package/docs/openapi.yaml +1291 -0
  111. package/docs/product-requirements.md +62 -0
  112. package/docs/requirements-v2.md +235 -0
  113. package/docs/roadmap-mvp.md +87 -0
  114. package/docs/sdk-api-reference.md +1108 -0
  115. package/docs/system-requirements.md +90 -0
  116. package/docs/system-spec-v2.md +1230 -0
  117. package/docs/tests/README.md +53 -0
  118. package/docs/tests/agent-qa-plan.md +63 -0
  119. package/docs/tests/browser-ui-tests.md +62 -0
  120. package/docs/tests/ci-quality-gates.md +48 -0
  121. package/docs/tests/coverage-model.md +64 -0
  122. package/docs/tests/e2e-scenario-tests.md +53 -0
  123. package/docs/tests/fixtures-test-data.md +63 -0
  124. package/docs/tests/observability-reliability-tests.md +54 -0
  125. package/docs/tests/product-test-matrix.md +145 -0
  126. package/docs/tests/qa-adoption-roadmap.md +130 -0
  127. package/docs/tests/qa-automation-plan.md +101 -0
  128. package/docs/tests/security-compliance-tests.md +57 -0
  129. package/docs/tests/test-framework-tools.md +88 -0
  130. package/docs/tests/test-suite-layout.md +121 -0
  131. package/docs/tests/unit-integration-tests.md +48 -0
  132. package/docs/todo-kyverno +714 -0
  133. package/docs/todos.md +4 -0
  134. package/docs/user-stories.md +78 -0
  135. package/docs/web-console-spec.md +533 -0
  136. package/examples/minikube-demo.yaml +190 -0
  137. package/examples/oam-application.yaml +23 -0
  138. package/examples/policy-kyverno-pr-title.yaml +18 -0
  139. package/package.json +66 -0
  140. package/scripts/build.mjs +29 -0
  141. package/scripts/setup-minikube.mjs +65 -0
  142. package/scripts/smoke.mjs +37 -0
  143. package/scripts/validate-doc-coverage.mjs +152 -0
  144. package/scripts/validate-package.mjs +95 -0
  145. package/scripts/validate-ui.mjs +305 -0
  146. package/src/agent-adapter-controller.js +169 -0
  147. package/src/agent-approval-controller.js +170 -0
  148. package/src/agent-context-bundles.js +242 -0
  149. package/src/agent-dispatch-controller.js +549 -0
  150. package/src/agent-gateway-config-controller.js +147 -0
  151. package/src/agent-identity-migration.js +115 -0
  152. package/src/agent-memory-controller.js +357 -0
  153. package/src/agent-memory-import.js +327 -0
  154. package/src/agent-memory-query.js +292 -0
  155. package/src/agent-memory-repository-source-controller.js +255 -0
  156. package/src/agent-mux-client.js +589 -0
  157. package/src/agent-permission-review.js +250 -0
  158. package/src/agent-persona-controller.js +135 -0
  159. package/src/agent-project-controller.js +117 -0
  160. package/src/agent-prompt-composition.js +55 -0
  161. package/src/agent-provider-config-controller.js +151 -0
  162. package/src/agent-secret-config-grant-controller.js +282 -0
  163. package/src/agent-session-transcript-controller.js +189 -0
  164. package/src/agent-stack-controller.js +421 -0
  165. package/src/agent-subagent-controller.js +160 -0
  166. package/src/agent-transport-binding-controller.js +121 -0
  167. package/src/agent-trigger-controller.js +387 -0
  168. package/src/agent-workspace-controller.js +702 -0
  169. package/src/agent-writeback-controller.js +302 -0
  170. package/src/api-controller.js +621 -0
  171. package/src/argocd-gitops.js +43 -0
  172. package/src/artifact-registry-controller.js +542 -0
  173. package/src/assistant-runtime.js +284 -0
  174. package/src/async-controller.js +207 -0
  175. package/src/audit-controller.js +191 -0
  176. package/src/auth.js +310 -0
  177. package/src/component-catalog.js +41 -0
  178. package/src/control-plane.js +136 -0
  179. package/src/controller-client.js +112 -0
  180. package/src/controller-ui.js +620 -0
  181. package/src/data-plane.js +179 -0
  182. package/src/event-bus.js +397 -0
  183. package/src/external/conflict-controller.js +225 -0
  184. package/src/external/github/auth.js +96 -0
  185. package/src/external/github/cicd.js +180 -0
  186. package/src/external/github/git-forge.js +240 -0
  187. package/src/external/github/index.js +144 -0
  188. package/src/external/github/issue-tracking.js +163 -0
  189. package/src/external/provider-adapter.js +161 -0
  190. package/src/external/provider-resource-factory.js +221 -0
  191. package/src/external/sync-controller.js +235 -0
  192. package/src/external/webhook-controller.js +144 -0
  193. package/src/external/write-controller.js +283 -0
  194. package/src/gitea-backend.js +131 -0
  195. package/src/gitea-service.js +173 -0
  196. package/src/handoff.js +98 -0
  197. package/src/health-probes.js +134 -0
  198. package/src/hooks-events.js +63 -0
  199. package/src/hooks-lifecycle.js +117 -0
  200. package/src/http-server.js +409 -0
  201. package/src/identity-policy.js +86 -0
  202. package/src/index.js +71 -0
  203. package/src/jitsi-agent-bridge.js +141 -0
  204. package/src/jitsi-meeting-controller.js +291 -0
  205. package/src/jitsi-sync-controller.js +198 -0
  206. package/src/kradle-inference-service-controller.js +246 -0
  207. package/src/kubernetes-controller-async.js +531 -0
  208. package/src/kubernetes-controller.js +904 -0
  209. package/src/kubernetes-resource-gateway.js +48 -0
  210. package/src/model-route-controller.js +364 -0
  211. package/src/notification-controller.js +178 -0
  212. package/src/operations.js +112 -0
  213. package/src/org-scoping.js +5 -0
  214. package/src/resource-model.js +282 -0
  215. package/src/runner-controller.js +272 -0
  216. package/src/runners-ci.js +48 -0
  217. package/src/runtime.js +196 -0
  218. package/src/snapshot-cache.js +157 -0
  219. package/src/virtual-model-controller.js +538 -0
  220. package/src/virtual-model-hook-bridge.js +200 -0
  221. package/src/web-ui.js +40 -0
  222. package/tests/agent-adapter-controller.test.js +361 -0
  223. package/tests/agent-approval-controller.test.js +173 -0
  224. package/tests/agent-context-bundles.test.js +278 -0
  225. package/tests/agent-dispatch-controller.test.js +679 -0
  226. package/tests/agent-gateway-config-controller.test.js +386 -0
  227. package/tests/agent-identity-migration.test.js +87 -0
  228. package/tests/agent-memory-controller.test.js +461 -0
  229. package/tests/agent-memory-import-snapshot.test.js +477 -0
  230. package/tests/agent-memory-query.test.js +404 -0
  231. package/tests/agent-memory-repository-source.test.js +514 -0
  232. package/tests/agent-mux-client.test.js +389 -0
  233. package/tests/agent-mux-integration.test.js +971 -0
  234. package/tests/agent-permission-review-v2.test.js +317 -0
  235. package/tests/agent-permission-review.test.js +209 -0
  236. package/tests/agent-persona-controller.test.js +127 -0
  237. package/tests/agent-project-controller.test.js +302 -0
  238. package/tests/agent-prompt-composition.test.js +76 -0
  239. package/tests/agent-provider-config-controller.test.js +376 -0
  240. package/tests/agent-resources.test.js +303 -0
  241. package/tests/agent-secret-config-grant.test.js +231 -0
  242. package/tests/agent-session-transcript-controller.test.js +499 -0
  243. package/tests/agent-stack-controller.test.js +283 -0
  244. package/tests/agent-subagent-controller.test.js +201 -0
  245. package/tests/agent-transport-binding-controller.test.js +294 -0
  246. package/tests/agent-trigger-controller.test.js +271 -0
  247. package/tests/agent-trigger-routes.test.js +190 -0
  248. package/tests/agent-trigger-sources.test.js +245 -0
  249. package/tests/agent-workspace-controller.test.js +181 -0
  250. package/tests/agent-writeback.test.js +292 -0
  251. package/tests/approval-persistence.test.js +171 -0
  252. package/tests/artifact-registry.test.js +511 -0
  253. package/tests/assistant-runtime.test.js +506 -0
  254. package/tests/async-controller.test.js +252 -0
  255. package/tests/audit-controller.test.js +227 -0
  256. package/tests/codespace-controller.test.js +318 -0
  257. package/tests/controller-client.test.js +133 -0
  258. package/tests/deployment.test.js +527 -0
  259. package/tests/e2e/lifecycle.test.js +120 -0
  260. package/tests/event-bus-integration.test.js +355 -0
  261. package/tests/external-github-forge.test.js +560 -0
  262. package/tests/external-github-issues-cicd.test.js +520 -0
  263. package/tests/external-integration.test.js +470 -0
  264. package/tests/external-persistence.test.js +415 -0
  265. package/tests/external-provider-adapter.test.js +365 -0
  266. package/tests/external-resource-model.test.js +223 -0
  267. package/tests/external-webhook-sync.test.js +287 -0
  268. package/tests/external-write-conflict.test.js +353 -0
  269. package/tests/gitea-service.test.js +253 -0
  270. package/tests/health-check-real.test.js +165 -0
  271. package/tests/health-probes.test.js +90 -0
  272. package/tests/hooks-lifecycle.test.js +364 -0
  273. package/tests/integration/full-flow.test.js +266 -0
  274. package/tests/jitsi-agent-bridge.test.js +119 -0
  275. package/tests/jitsi-helm-integration.test.js +77 -0
  276. package/tests/jitsi-meeting-controller.test.js +170 -0
  277. package/tests/jitsi-resource-model.test.js +73 -0
  278. package/tests/jitsi-sync-controller.test.js +112 -0
  279. package/tests/kradle-inference-service.test.js +689 -0
  280. package/tests/kradle.test.js +779 -0
  281. package/tests/memory-search-wiring.test.js +270 -0
  282. package/tests/model-route-controller.test.js +733 -0
  283. package/tests/notification-controller.test.js +196 -0
  284. package/tests/notification-integration.test.js +179 -0
  285. package/tests/org-scoping.test.js +687 -0
  286. package/tests/runner-controller.test.js +327 -0
  287. package/tests/runner-integration.test.js +231 -0
  288. package/tests/session-cookie-hmac.test.js +151 -0
  289. package/tests/snapshot-performance.test.js +315 -0
  290. package/tests/sse-events.test.js +107 -0
  291. package/tests/virtual-model-controller.test.js +877 -0
  292. package/tests/virtual-model-hook-bridge.test.js +384 -0
  293. package/tests/webhook-trigger.test.js +198 -0
  294. package/tests/workspace-volumes.test.js +312 -0
  295. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,364 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import { createHooksLifecycleEmitter, HOOKS_LIFECYCLE_BOUNDARY } from '../src/hooks-lifecycle.js';
4
+ import { createResource } from '../src/resource-model.js';
5
+ import { createAgentDispatchController } from '../src/agent-dispatch-controller.js';
6
+
7
+ function makeEventBus() {
8
+ const events = [];
9
+ return {
10
+ emit(event) { events.push(event); },
11
+ events,
12
+ };
13
+ }
14
+
15
+ function makeRun(name, spec = {}, status = {}) {
16
+ return createResource('AgentDispatchRun',
17
+ { name, namespace: 'kradle-org-default' },
18
+ { organizationRef: 'default', repository: 'repo', sourceRefs: [], agentStack: 'stack-a', taskKind: 'diagnostic', ...spec },
19
+ { phase: 'Pending', queuedAt: new Date().toISOString(), ...status }
20
+ );
21
+ }
22
+
23
+ function makeApproval(name, spec = {}, status = {}) {
24
+ return createResource('AgentApproval',
25
+ { name, namespace: 'kradle-org-default' },
26
+ { organizationRef: 'default', dispatchRun: 'run-1', action: 'secret-access', requestedBy: 'user-1', ...spec },
27
+ { phase: 'Pending', ...status }
28
+ );
29
+ }
30
+
31
+ function makeWorkspace(name, spec = {}) {
32
+ return createResource('KradleWorkspace',
33
+ { name, namespace: 'kradle-org-default' },
34
+ { organizationRef: 'default', repository: 'repo', volumeSpec: {}, ...spec },
35
+ { phase: 'Ready' }
36
+ );
37
+ }
38
+
39
+ // ─── Factory tests ─────────────────────────────────────────────────────────
40
+
41
+ describe('HOOKS_LIFECYCLE_BOUNDARY', () => {
42
+ it('declares expected role and scope', () => {
43
+ assert.equal(HOOKS_LIFECYCLE_BOUNDARY.role, 'hooks-lifecycle');
44
+ assert.ok(HOOKS_LIFECYCLE_BOUNDARY.scope.includes('Lifecycle event emission'));
45
+ });
46
+ });
47
+
48
+ describe('createHooksLifecycleEmitter — factory', () => {
49
+ it('throws when eventBus is null', () => {
50
+ assert.throws(
51
+ () => createHooksLifecycleEmitter(null),
52
+ /requires an eventBus/
53
+ );
54
+ });
55
+
56
+ it('throws when eventBus has no emit method', () => {
57
+ assert.throws(
58
+ () => createHooksLifecycleEmitter({ subscribe: () => {} }),
59
+ /requires an eventBus/
60
+ );
61
+ });
62
+
63
+ it('returns object with all 9 emitter methods', () => {
64
+ const bus = makeEventBus();
65
+ const emitter = createHooksLifecycleEmitter(bus);
66
+ const methods = [
67
+ 'emitRunCreated', 'emitRunCompleted', 'emitStepStarted', 'emitStepEnded',
68
+ 'emitApprovalRequested', 'emitApprovalDecided', 'emitWorkspaceProvisioned',
69
+ 'emitSessionStarted', 'emitSessionEnded',
70
+ ];
71
+ for (const method of methods) {
72
+ assert.equal(typeof emitter[method], 'function', `missing method: ${method}`);
73
+ }
74
+ });
75
+ });
76
+
77
+ // ─── emitRunCreated ─────────────────────────────────────────────────────────
78
+
79
+ describe('createHooksLifecycleEmitter — emitRunCreated', () => {
80
+ it('emits RUN_CREATED with runId, stack, and timestamp', () => {
81
+ const bus = makeEventBus();
82
+ const emitter = createHooksLifecycleEmitter(bus);
83
+ const run = makeRun('test-run-1');
84
+ emitter.emitRunCreated(run);
85
+
86
+ assert.equal(bus.events.length, 1);
87
+ const ev = bus.events[0];
88
+ assert.equal(ev.type, 'hook');
89
+ assert.equal(ev.event, 'RUN_CREATED');
90
+ assert.equal(ev.runId, 'test-run-1');
91
+ assert.equal(ev.stack, 'stack-a');
92
+ assert.ok(ev.timestamp);
93
+ });
94
+ });
95
+
96
+ // ─── emitRunCompleted ───────────────────────────────────────────────────────
97
+
98
+ describe('createHooksLifecycleEmitter — emitRunCompleted', () => {
99
+ it('emits RUN_COMPLETED with result and null duration when no queuedAt', () => {
100
+ const bus = makeEventBus();
101
+ const emitter = createHooksLifecycleEmitter(bus);
102
+ const run = createResource('AgentDispatchRun', { name: 'r2' }, { organizationRef: 'default', repository: 'repo', sourceRefs: [], agentStack: 'stack-b', taskKind: 'diagnostic' }, { phase: 'Completed' });
103
+ emitter.emitRunCompleted(run, { phase: 'Completed' });
104
+
105
+ const ev = bus.events[0];
106
+ assert.equal(ev.event, 'RUN_COMPLETED');
107
+ assert.equal(ev.result, 'Completed');
108
+ assert.equal(ev.duration, null);
109
+ });
110
+
111
+ it('emits RUN_COMPLETED with duration computed from run.status.queuedAt', () => {
112
+ const bus = makeEventBus();
113
+ const emitter = createHooksLifecycleEmitter(bus);
114
+ const run = makeRun('r3', {}, { queuedAt: new Date(Date.now() - 5000).toISOString() });
115
+ emitter.emitRunCompleted(run, { phase: 'Completed' });
116
+
117
+ const ev = bus.events[0];
118
+ assert.ok(ev.duration >= 4000, 'duration should be at least 4000ms');
119
+ assert.ok(ev.duration < 10000, 'duration should be less than 10000ms');
120
+ });
121
+ });
122
+
123
+ // ─── emitStepStarted / emitStepEnded ────────────────────────────────────────
124
+
125
+ describe('createHooksLifecycleEmitter — emitStepStarted / emitStepEnded', () => {
126
+ it('emitStepStarted emits STEP_STARTED with runId and step', () => {
127
+ const bus = makeEventBus();
128
+ const emitter = createHooksLifecycleEmitter(bus);
129
+ const run = makeRun('r4');
130
+ emitter.emitStepStarted(run, 'launch');
131
+
132
+ const ev = bus.events[0];
133
+ assert.equal(ev.event, 'STEP_STARTED');
134
+ assert.equal(ev.runId, 'r4');
135
+ assert.equal(ev.step, 'launch');
136
+ assert.ok(ev.timestamp);
137
+ });
138
+
139
+ it('emitStepEnded emits STEP_ENDED with runId, step, and result', () => {
140
+ const bus = makeEventBus();
141
+ const emitter = createHooksLifecycleEmitter(bus);
142
+ const run = makeRun('r5');
143
+ emitter.emitStepEnded(run, 'launch', { success: true });
144
+
145
+ const ev = bus.events[0];
146
+ assert.equal(ev.event, 'STEP_ENDED');
147
+ assert.equal(ev.runId, 'r5');
148
+ assert.equal(ev.step, 'launch');
149
+ assert.deepEqual(ev.result, { success: true });
150
+ });
151
+ });
152
+
153
+ // ─── emitApprovalRequested / emitApprovalDecided ────────────────────────────
154
+
155
+ describe('createHooksLifecycleEmitter — emitApprovalRequested / emitApprovalDecided', () => {
156
+ it('emitApprovalRequested emits APPROVAL_REQUESTED with approvalId, action, requestedBy', () => {
157
+ const bus = makeEventBus();
158
+ const emitter = createHooksLifecycleEmitter(bus);
159
+ const approval = makeApproval('appr-1');
160
+ emitter.emitApprovalRequested(approval);
161
+
162
+ const ev = bus.events[0];
163
+ assert.equal(ev.event, 'APPROVAL_REQUESTED');
164
+ assert.equal(ev.approvalId, 'appr-1');
165
+ assert.equal(ev.action, 'secret-access');
166
+ assert.equal(ev.requestedBy, 'user-1');
167
+ });
168
+
169
+ it('emitApprovalDecided emits APPROVAL_DECIDED with decision', () => {
170
+ const bus = makeEventBus();
171
+ const emitter = createHooksLifecycleEmitter(bus);
172
+ const approval = makeApproval('appr-2', {}, { decision: 'approved' });
173
+ emitter.emitApprovalDecided(approval);
174
+
175
+ const ev = bus.events[0];
176
+ assert.equal(ev.event, 'APPROVAL_DECIDED');
177
+ assert.equal(ev.approvalId, 'appr-2');
178
+ assert.equal(ev.decision, 'approved');
179
+ });
180
+ });
181
+
182
+ // ─── emitWorkspaceProvisioned ────────────────────────────────────────────────
183
+
184
+ describe('createHooksLifecycleEmitter — emitWorkspaceProvisioned', () => {
185
+ it('emits WORKSPACE_PROVISIONED with workspaceId and repository', () => {
186
+ const bus = makeEventBus();
187
+ const emitter = createHooksLifecycleEmitter(bus);
188
+ const workspace = makeWorkspace('ws-1', { repository: 'my-repo' });
189
+ emitter.emitWorkspaceProvisioned(workspace);
190
+
191
+ const ev = bus.events[0];
192
+ assert.equal(ev.event, 'WORKSPACE_PROVISIONED');
193
+ assert.equal(ev.workspaceId, 'ws-1');
194
+ assert.equal(ev.repository, 'my-repo');
195
+ });
196
+ });
197
+
198
+ // ─── emitSessionStarted / emitSessionEnded ───────────────────────────────────
199
+
200
+ describe('createHooksLifecycleEmitter — emitSessionStarted / emitSessionEnded', () => {
201
+ it('emitSessionStarted emits SESSION_STARTED with sessionId and runId', () => {
202
+ const bus = makeEventBus();
203
+ const emitter = createHooksLifecycleEmitter(bus);
204
+ emitter.emitSessionStarted({ sessionId: 'sess-abc', runId: 'run-xyz' });
205
+
206
+ const ev = bus.events[0];
207
+ assert.equal(ev.event, 'SESSION_STARTED');
208
+ assert.equal(ev.sessionId, 'sess-abc');
209
+ assert.equal(ev.runId, 'run-xyz');
210
+ });
211
+
212
+ it('emitSessionEnded emits SESSION_ENDED with sessionId and runId', () => {
213
+ const bus = makeEventBus();
214
+ const emitter = createHooksLifecycleEmitter(bus);
215
+ emitter.emitSessionEnded({ sessionId: 'sess-abc', runId: 'run-xyz' });
216
+
217
+ const ev = bus.events[0];
218
+ assert.equal(ev.event, 'SESSION_ENDED');
219
+ assert.equal(ev.sessionId, 'sess-abc');
220
+ assert.equal(ev.runId, 'run-xyz');
221
+ });
222
+ });
223
+
224
+ // ─── Wiring in agent-dispatch-controller ────────────────────────────────────
225
+
226
+ describe('agent-dispatch-controller — lifecycleEmitter wiring', () => {
227
+ function buildValidResources(stackName) {
228
+ const stack = createResource('AgentStack', { name: stackName, namespace: 'kradle-org-default' }, {
229
+ organizationRef: 'default',
230
+ baseAgent: 'claude-code',
231
+ adapter: 'claude-code',
232
+ runtimeIdentity: { serviceAccountRef: 'sa-default' },
233
+ });
234
+ const sa = createResource('AgentServiceAccount', { name: 'sa-default', namespace: 'kradle-org-default' }, {
235
+ organizationRef: 'default', namespace: 'kradle-org-default', serviceAccountName: 'sa-default',
236
+ });
237
+ const rb = createResource('AgentRoleBinding', { name: 'rb-1', namespace: 'kradle-org-default' }, {
238
+ organizationRef: 'default', subject: 'sa-default', roleRef: 'agent-developer', scope: 'namespace',
239
+ });
240
+ const sg = createResource('AgentSecretGrant', { name: 'sg-1', namespace: 'kradle-org-default' }, {
241
+ organizationRef: 'default', subject: 'sa-default', secretRef: 'secret-model-provider', purpose: 'model-provider',
242
+ });
243
+ return { AgentStack: [stack], AgentServiceAccount: [sa], AgentRoleBinding: [rb], AgentSecretGrant: [sg] };
244
+ }
245
+
246
+ it('emits RUN_CREATED via lifecycleEmitter during createManualDispatch', async () => {
247
+ const bus = makeEventBus();
248
+ const emittedEvents = [];
249
+ const mockLifecycleEmitter = {
250
+ emitRunCreated(run) { emittedEvents.push({ event: 'RUN_CREATED', runId: run.metadata?.name }); },
251
+ emitRunCompleted() {},
252
+ emitStepStarted() {},
253
+ emitStepEnded() {},
254
+ emitApprovalRequested() {},
255
+ emitApprovalDecided() {},
256
+ emitWorkspaceProvisioned() {},
257
+ emitSessionStarted() {},
258
+ emitSessionEnded() {},
259
+ };
260
+
261
+ // Mock mux client that returns no resource gateway (no job submission)
262
+ const mockMuxClient = {
263
+ isAvailable() { return false; },
264
+ createAgentJob() { throw new Error('no resource gateway'); },
265
+ submitAgentJob() { throw new Error('no resource gateway'); },
266
+ };
267
+
268
+ const controller = createAgentDispatchController({
269
+ lifecycleEmitter: mockLifecycleEmitter,
270
+ agentMuxClient: mockMuxClient,
271
+ });
272
+
273
+ const resources = buildValidResources('stack-lc');
274
+ const result = await controller.createManualDispatch({
275
+ repository: 'test-repo',
276
+ ref: 'main',
277
+ agentStack: 'stack-lc',
278
+ actor: 'test-user',
279
+ namespace: 'kradle-org-default',
280
+ organizationRef: 'default',
281
+ resources,
282
+ });
283
+
284
+ assert.equal(result.error, false);
285
+ const created = emittedEvents.find(e => e.event === 'RUN_CREATED');
286
+ assert.ok(created, 'RUN_CREATED should have been emitted');
287
+ assert.ok(created.runId, 'RUN_CREATED should include runId');
288
+ });
289
+
290
+ it('emits WORKSPACE_PROVISIONED when workspace is created during createManualDispatch', async () => {
291
+ const emittedEvents = [];
292
+ const mockLifecycleEmitter = {
293
+ emitRunCreated() {},
294
+ emitRunCompleted() {},
295
+ emitStepStarted() {},
296
+ emitStepEnded() {},
297
+ emitApprovalRequested() {},
298
+ emitApprovalDecided() {},
299
+ emitWorkspaceProvisioned(ws) { emittedEvents.push({ event: 'WORKSPACE_PROVISIONED', workspaceId: ws.metadata?.name }); },
300
+ emitSessionStarted() {},
301
+ emitSessionEnded() {},
302
+ };
303
+
304
+ const mockMuxClient = {
305
+ isAvailable() { return false; },
306
+ createAgentJob() { throw new Error('no resource gateway'); },
307
+ submitAgentJob() { throw new Error('no resource gateway'); },
308
+ };
309
+
310
+ const controller = createAgentDispatchController({
311
+ lifecycleEmitter: mockLifecycleEmitter,
312
+ agentMuxClient: mockMuxClient,
313
+ });
314
+
315
+ const resources = buildValidResources('stack-ws');
316
+ const result = await controller.createManualDispatch({
317
+ repository: 'test-repo',
318
+ ref: 'main',
319
+ agentStack: 'stack-ws',
320
+ actor: 'test-user',
321
+ namespace: 'kradle-org-default',
322
+ organizationRef: 'default',
323
+ resources,
324
+ });
325
+
326
+ assert.equal(result.error, false);
327
+ const wsEvent = emittedEvents.find(e => e.event === 'WORKSPACE_PROVISIONED');
328
+ assert.ok(wsEvent, 'WORKSPACE_PROVISIONED should have been emitted');
329
+ });
330
+
331
+ it('emits RUN_COMPLETED via lifecycleEmitter when persistSessionEvent receives completion event', () => {
332
+ const emittedEvents = [];
333
+ const mockLifecycleEmitter = {
334
+ emitRunCreated() {},
335
+ emitRunCompleted(run, result) { emittedEvents.push({ event: 'RUN_COMPLETED', result: result.phase }); },
336
+ emitStepStarted() {},
337
+ emitStepEnded() {},
338
+ emitApprovalRequested() {},
339
+ emitApprovalDecided() {},
340
+ emitWorkspaceProvisioned() {},
341
+ emitSessionStarted() {},
342
+ emitSessionEnded(session) { emittedEvents.push({ event: 'SESSION_ENDED', sessionId: session.sessionId }); },
343
+ };
344
+
345
+ const controller = createAgentDispatchController({ lifecycleEmitter: mockLifecycleEmitter });
346
+ const run = makeRun('run-persist');
347
+ const attempt = createResource('AgentDispatchAttempt', { name: 'attempt-1' }, {
348
+ organizationRef: 'default', agentDispatchRun: 'run-persist', attemptReason: 'initial',
349
+ agentStackSnapshot: {},
350
+ }, { agentMuxSessionId: 'sess-xyz', agentMuxRunId: 'amux-run-1' });
351
+
352
+ controller.persistSessionEvent(
353
+ { type: 'completion', role: 'system', content: 'done' },
354
+ run, attempt, { namespace: 'default', organizationRef: 'default' }
355
+ );
356
+
357
+ const completed = emittedEvents.find(e => e.event === 'RUN_COMPLETED');
358
+ assert.ok(completed, 'RUN_COMPLETED should have been emitted');
359
+ assert.equal(completed.result, 'Completed');
360
+
361
+ const ended = emittedEvents.find(e => e.event === 'SESSION_ENDED');
362
+ assert.ok(ended, 'SESSION_ENDED should have been emitted on completion');
363
+ });
364
+ });
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Core Integration Tests — End-to-end data flow tests using mock kubectl gateway
3
+ *
4
+ * Tests the full data flow through the Kradle API controller using a mock
5
+ * resource gateway that stores resources in a Map instead of kubectl.
6
+ */
7
+ import assert from 'node:assert/strict';
8
+ import test from 'node:test';
9
+ import {
10
+ createKradleApiController,
11
+ createResource,
12
+ createAuditController,
13
+ createAgentSecretGrantController,
14
+ createAgentMemoryController,
15
+ listGrantsForAgent,
16
+ } from '../../src/index.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Mock resource gateway — stores resources in a Map instead of kubectl
20
+ // ---------------------------------------------------------------------------
21
+
22
+ function createMockGateway() {
23
+ const store = new Map();
24
+ return {
25
+ namespace: 'kradle-org-test',
26
+ async apply(resource) {
27
+ store.set(`${resource.kind}/${resource.metadata.name}`, resource);
28
+ return { operation: 'apply', resource };
29
+ },
30
+ async list(kind) {
31
+ return { items: [...store.values()].filter((r) => r.kind === kind) };
32
+ },
33
+ async get(kind, name) {
34
+ return { resource: store.get(`${kind}/${name}`) || null };
35
+ },
36
+ async delete(kind, name) {
37
+ store.delete(`${kind}/${name}`);
38
+ return { operation: 'delete' };
39
+ },
40
+ async snapshot() {
41
+ return { namespace: 'kradle-org-test', resources: {} };
42
+ },
43
+ watch() {
44
+ return { close: () => {} };
45
+ },
46
+ resourceDefinitions: [],
47
+ };
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Test 1: Org/user/repo flow
52
+ // ---------------------------------------------------------------------------
53
+
54
+ test('full flow: create org → user → repository → list shows repo', async () => {
55
+ const gw = createMockGateway();
56
+ const controller = createKradleApiController({ resourceGateway: gw });
57
+
58
+ // Create org
59
+ const org = createResource('Organization', { name: 'test-org', namespace: 'kradle-system' }, { displayName: 'Test Org', namespaceName: 'kradle-org-test-org' });
60
+ await controller.applyResource(org);
61
+
62
+ // Create user
63
+ const user = createResource('User', { name: 'alice', namespace: 'kradle-org-test-org' }, { organizationRef: 'test-org', displayName: 'Alice', email: 'alice@example.com' });
64
+ await controller.applyResource(user);
65
+
66
+ // Create repository
67
+ const repo = createResource('Repository', { name: 'my-repo', namespace: 'kradle-org-test-org' }, { organizationRef: 'test-org', visibility: 'internal' });
68
+ await controller.applyResource(repo);
69
+
70
+ // List repositories — should include the new repo
71
+ const result = await controller.listResource('Repository');
72
+ assert.ok(Array.isArray(result.items), 'result.items must be an array');
73
+ const found = result.items.find((r) => r.metadata?.name === 'my-repo');
74
+ assert.ok(found, 'my-repo should appear in listResource(Repository)');
75
+ assert.equal(found.spec?.organizationRef, 'test-org');
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Test 2: Agent stack + trigger rule flow
80
+ // ---------------------------------------------------------------------------
81
+
82
+ test('full flow: create agent stack → create trigger rule → both appear in list', async () => {
83
+ const gw = createMockGateway();
84
+ const controller = createKradleApiController({ resourceGateway: gw });
85
+
86
+ // Create agent stack
87
+ const stack = createResource(
88
+ 'AgentStack',
89
+ { name: 'review-bot', namespace: 'kradle-org-myorg' },
90
+ { organizationRef: 'myorg', baseAgent: 'claude-code', adapter: 'default', runtimeIdentity: 'workspace' }
91
+ );
92
+ await controller.applyResource(stack);
93
+
94
+ // Create trigger rule
95
+ const rule = createResource(
96
+ 'AgentTriggerRule',
97
+ { name: 'pr-trigger', namespace: 'kradle-org-myorg' },
98
+ { organizationRef: 'myorg', sources: [{ type: 'pull_request', events: ['opened'] }], agentStack: 'review-bot', taskKind: 'code-review' }
99
+ );
100
+ await controller.applyResource(rule);
101
+
102
+ // Verify both appear
103
+ const stackList = await controller.listResource('AgentStack');
104
+ assert.ok(stackList.items.some((s) => s.metadata?.name === 'review-bot'), 'review-bot stack should appear');
105
+
106
+ const ruleList = await controller.listResource('AgentTriggerRule');
107
+ assert.ok(ruleList.items.some((r) => r.metadata?.name === 'pr-trigger'), 'pr-trigger rule should appear');
108
+ });
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Test 3: Workspace flow
112
+ // ---------------------------------------------------------------------------
113
+
114
+ test('full flow: create workspace → verify it appears in list', async () => {
115
+ const gw = createMockGateway();
116
+ const controller = createKradleApiController({ resourceGateway: gw });
117
+
118
+ // Create workspace
119
+ const workspace = createResource(
120
+ 'KradleWorkspace',
121
+ { name: 'dev-workspace', namespace: 'kradle-org-myorg' },
122
+ { organizationRef: 'myorg', repository: 'my-repo', volumeSpec: { storageClass: 'standard', size: '10Gi' } }
123
+ );
124
+ await controller.applyResource(workspace);
125
+
126
+ // List workspaces — should include the new one
127
+ const result = await controller.listResource('KradleWorkspace');
128
+ assert.ok(Array.isArray(result.items));
129
+ const found = result.items.find((r) => r.metadata?.name === 'dev-workspace');
130
+ assert.ok(found, 'dev-workspace should appear in listResource(KradleWorkspace)');
131
+ assert.equal(found.spec?.repository, 'my-repo');
132
+ });
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Test 4: Secret grant flow
136
+ // ---------------------------------------------------------------------------
137
+
138
+ test('full flow: create secret grant → listGrantsForAgent returns it', async () => {
139
+ const gw = createMockGateway();
140
+ const ctrl = createAgentSecretGrantController();
141
+
142
+ // Create a secret grant
143
+ const result = ctrl.createSecretGrant({
144
+ name: 'db-pass-grant',
145
+ orgRef: 'myorg',
146
+ secretName: 'db-password',
147
+ grantedTo: 'review-bot',
148
+ permissions: ['read'],
149
+ namespace: 'kradle-org-myorg',
150
+ });
151
+
152
+ assert.ok(result.grant, 'createSecretGrant must return a grant');
153
+ assert.equal(result.grant.kind, 'AgentSecretGrant');
154
+ assert.equal(result.grant.metadata.name, 'db-pass-grant');
155
+ assert.equal(result.grant.spec.grantedTo, 'review-bot');
156
+
157
+ // Apply the grant resource via mock gateway
158
+ await gw.apply(result.grant);
159
+
160
+ // List grants and filter for this agent
161
+ const list = await gw.list('AgentSecretGrant');
162
+ const agentGrants = listGrantsForAgent(list.items, 'review-bot');
163
+ assert.ok(agentGrants.length >= 1, 'listGrantsForAgent should return at least 1 grant for review-bot');
164
+ assert.equal(agentGrants[0].metadata.name, 'db-pass-grant');
165
+ });
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Test 5: External flow — syncExternalBinding
169
+ // ---------------------------------------------------------------------------
170
+
171
+ test('full flow: syncExternalBinding creates resource with external envelope', async () => {
172
+ const gw = createMockGateway();
173
+ const controller = createKradleApiController({ resourceGateway: gw });
174
+
175
+ const result = await controller.syncExternalBinding('github-binding', {
176
+ kind: 'Repository',
177
+ localName: 'synced-repo',
178
+ namespace: 'default',
179
+ spec: { organizationRef: 'default', visibility: 'private' },
180
+ externalEnvelope: {
181
+ nativeId: 'gh-repo-42',
182
+ url: 'https://github.com/org/synced-repo',
183
+ etag: '"abc123"',
184
+ providerRef: 'github-provider',
185
+ },
186
+ });
187
+
188
+ assert.ok(result, 'syncExternalBinding must return a result');
189
+ assert.ok(result.resource, 'result must include the upserted resource');
190
+ assert.equal(
191
+ result.resource.status?.external?.nativeId || result.resource?.status?.external?.nativeId,
192
+ 'gh-repo-42',
193
+ 'upserted resource must have correct nativeId'
194
+ );
195
+ });
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Test 6: Memory flow — create repository source → verify in list
199
+ // ---------------------------------------------------------------------------
200
+
201
+ test('full flow: create memory repository → apply → appears in list', async () => {
202
+ const gw = createMockGateway();
203
+ const controller = createKradleApiController({ resourceGateway: gw });
204
+
205
+ // Create a memory repository resource
206
+ const memRepo = createResource(
207
+ 'AgentMemoryRepository',
208
+ { name: 'org-brain', namespace: 'kradle-org-myorg' },
209
+ { organizationRef: 'myorg', repositoryRef: 'memory-repo', defaultBranch: 'main', layoutProfile: 'standard' }
210
+ );
211
+ await controller.applyResource(memRepo);
212
+
213
+ // Create a memory source
214
+ const memSource = createResource(
215
+ 'AgentMemorySource',
216
+ { name: 'codebase-source', namespace: 'kradle-org-myorg' },
217
+ {
218
+ organizationRef: 'myorg',
219
+ repositoryRef: 'org-brain',
220
+ appliesTo: { stacks: ['review-bot'] },
221
+ include: ['decisions/', 'patterns/'],
222
+ }
223
+ );
224
+ await controller.applyResource(memSource);
225
+
226
+ // Verify both appear in their lists
227
+ const repoList = await controller.listResource('AgentMemoryRepository');
228
+ assert.ok(repoList.items.some((r) => r.metadata?.name === 'org-brain'), 'org-brain memory repo should appear');
229
+
230
+ const sourceList = await controller.listResource('AgentMemorySource');
231
+ assert.ok(sourceList.items.some((s) => s.metadata?.name === 'codebase-source'), 'codebase-source should appear');
232
+ });
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Test 7: Audit flow — apply resource → audit log contains event
236
+ // ---------------------------------------------------------------------------
237
+
238
+ test('full flow: apply resource with onAuditEvent → audit log contains the event', async () => {
239
+ const gw = createMockGateway();
240
+ const audit = createAuditController();
241
+
242
+ const controller = createKradleApiController({
243
+ resourceGateway: gw,
244
+ onAuditEvent: (evt) => audit.log({
245
+ org: evt.org || 'audited',
246
+ actor: 'system',
247
+ action: evt.operation || 'apply',
248
+ resource: { kind: evt.kind, name: evt.name },
249
+ timestamp: evt.timestamp,
250
+ }),
251
+ });
252
+
253
+ // Apply a resource
254
+ const repo = createResource(
255
+ 'Repository',
256
+ { name: 'audited-repo', namespace: 'kradle-org-audited' },
257
+ { organizationRef: 'audited', visibility: 'private' }
258
+ );
259
+ await controller.applyResource(repo);
260
+
261
+ // Verify audit log contains the event
262
+ const { events, total } = audit.query({ org: 'audited' });
263
+ assert.ok(total >= 1, 'audit log must contain at least 1 event after apply');
264
+ assert.equal(events[0].action, 'apply', 'audit event action must be "apply"');
265
+ assert.equal(events[0].org, 'audited', 'audit event org must match');
266
+ });