@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,120 @@
1
+ import test from 'node:test';
2
+
3
+ import assert from 'node:assert/strict';
4
+
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+
9
+ import { buildMinikubePlan } from '../../scripts/setup-minikube.mjs';
10
+
11
+
12
+
13
+ const requiredChartFiles = [
14
+
15
+ '../charts/Chart.yaml',
16
+
17
+ '../charts/values.yaml',
18
+
19
+ '../charts/crds/repositories.yaml',
20
+
21
+ '../charts/crds/aggregated-resources.yaml',
22
+
23
+ '../charts/templates/apiservice.yaml',
24
+
25
+ '../charts/templates/deployments.yaml',
26
+
27
+ '../charts/templates/rbac.yaml',
28
+
29
+ '../charts/templates/services.yaml',
30
+
31
+
32
+ '../charts/templates/networkpolicy.yaml',
33
+ '../charts/templates/gitea.yaml',
34
+ '../charts/templates/argocd-application.yaml',
35
+
36
+ 'examples/minikube-demo.yaml'
37
+
38
+ ];
39
+
40
+
41
+
42
+ test('chart package contains the MVP Kubernetes install surface', () => {
43
+
44
+ for (const file of requiredChartFiles) assert.equal(existsSync(file), true, `${file} exists`);
45
+
46
+ const chart = requiredChartFiles.map((file) => readFileSync(file, 'utf8')).join('\n');
47
+ const values = readFileSync('../charts/values.yaml', 'utf8');
48
+
49
+ for (const kind of ['CustomResourceDefinition', 'Deployment', 'Service', 'ServiceAccount', 'ClusterRole', 'NetworkPolicy', 'PersistentVolumeClaim']) assert.ok(chart.includes(`kind: ${kind}`), `chart includes ${kind}`);
50
+
51
+ for (const resource of ['Organization', 'Repository', 'SSHKey', 'RepositoryPermission', 'BranchProtection', 'RefPolicy', 'PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'RunnerPool', 'WebhookSubscription', 'WebhookDelivery', 'View', 'Selector']) assert.ok(chart.includes(`kind: ${resource}`), `package covers ${resource}`);
52
+ assert.ok(chart.includes('kind: Pipeline'), 'package includes demo Pipeline workflow');
53
+ assert.ok(chart.includes('kind: Application'), 'package includes Argo CD Application');
54
+ assert.ok(chart.includes('kradle-kubevela'), 'package installs KubeVela through Argo CD');
55
+ assert.ok(chart.includes('core.oam.dev'), 'Kradle service account can manage delivery resources');
56
+ assert.ok(chart.includes('create') && chart.includes('delete'), 'Kradle service account can compose and remove delivery resources');
57
+ assert.ok(chart.includes('vela-core'), 'package references upstream KubeVela vela-core chart');
58
+ assert.ok(chart.includes('apiService') && chart.includes('enabled: false'), 'APIService registration is optional for CRD-backed local installs');
59
+ assert.ok(chart.includes('gitea/gitea'), 'package includes Gitea backend image');
60
+ assert.ok(chart.includes('readinessProbe'), 'workloads expose readiness probes for live installs');
61
+ assert.ok(chart.includes('serviceAccountName'), 'workloads run with the Kradle service account');
62
+ assert.ok(chart.includes('customresourcedefinitions'), 'service account can discover installed Kradle CRDs');
63
+ assert.ok(chart.includes('kradle.webImage'), 'web deployment uses the web container image');
64
+ assert.ok(chart.includes('sshkeys.kradle.a5c.ai'), 'package includes SSH key reconciliation resources');
65
+ assert.ok(chart.includes('repositorypermissions.kradle.a5c.ai'), 'package includes Gitea permission reconciliation resources');
66
+ assert.ok(chart.includes('revoked'), 'package access CRDs allow revocation state');
67
+ assert.ok(chart.includes('KRADLE_CONTROLLER_URL'), 'web deployment points at the in-cluster controller API');
68
+ assert.ok(values.includes('port: 80'), 'controller API service exposes an HTTP service port for in-cluster web fetches');
69
+ assert.ok(!values.includes('port: 443'), 'controller API does not expose plain HTTP through a TLS-looking service port');
70
+ assert.ok(chart.includes('containerPort: 2222'), 'rootless Gitea SSH service targets port 2222');
71
+ assert.ok(chart.includes('kradle.fullname') && chart.includes('gitea-http'), 'Gitea and Argo CD URLs derive the release-scoped service name');
72
+ assert.ok(chart.includes('kradle.a5c.ai/gitops-engine: argocd'), 'package labels Argo CD GitOps engine');
73
+ assert.ok(!chart.includes('`n'), 'package values use real YAML newlines');
74
+ assert.ok(chart.includes('.Values.argocd.syncPolicy.prune'), 'Argo CD template consumes syncPolicy.prune');
75
+ assert.ok(chart.includes('.Values.argocd.syncPolicy.selfHeal'), 'Argo CD template consumes syncPolicy.selfHeal');
76
+ assert.ok(chart.includes('- CreateNamespace=true'), 'values expose Argo CD CreateNamespace sync option');
77
+ for (const valueTerm of ['externalDependencies', 'objectStorage', 'oidc', 'gatekeeper', 'autoscaling', 'targetCPUUtilizationPercentage', 'gitea', 'argocd', 'repoURL', 'syncPolicy']) assert.ok(chart.includes(valueTerm), `values expose ${valueTerm}`);
78
+ });
79
+
80
+
81
+
82
+ test('minikube setup dry-run exposes deterministic local install commands', () => {
83
+
84
+ const plan = buildMinikubePlan({ apply: false, json: true, profile: 'kradle-test', namespace: 'kradle-system', release: 'kradle', driver: 'docker', chart: '../charts' });
85
+
86
+ const commands = plan.commands.map(([cmd, args]) => [cmd, ...args].join(' '));
87
+
88
+ assert.equal(plan.mode, 'dry-run');
89
+
90
+ assert.ok(commands.some((command) => command.startsWith('minikube start -p kradle-test')));
91
+
92
+ assert.ok(commands.includes('helm lint ../charts'));
93
+
94
+ assert.ok(commands.some((command) => command.includes('helm upgrade --install kradle ../charts')));
95
+
96
+ assert.ok(commands.includes('kubectl apply -n kradle-system -f examples/minikube-demo.yaml'));
97
+
98
+ assert.ok(commands.includes('node scripts/smoke.mjs'));
99
+
100
+ });
101
+
102
+
103
+
104
+ test('setup:minikube --dry-run --json is machine-readable', () => {
105
+
106
+ const output = execFileSync(process.execPath, ['scripts/setup-minikube.mjs', '--dry-run', '--json', '--profile=kradle-test'], { encoding: 'utf8' });
107
+
108
+ const parsed = JSON.parse(output);
109
+
110
+ assert.equal(parsed.mode, 'dry-run');
111
+
112
+ assert.ok(parsed.requiredTools.includes('minikube'));
113
+
114
+ assert.ok(parsed.commands.some((command) => command.includes('helm lint ../charts')));
115
+
116
+ });
117
+
118
+
119
+
120
+
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Event bus integration tests
3
+ *
4
+ * Exercises the event bus pub/sub mechanism with multiple subscribers,
5
+ * emitResourceChange field validation, unsubscribe isolation, and the
6
+ * globalEventBus singleton.
7
+ */
8
+ import assert from 'node:assert/strict';
9
+ import test from 'node:test';
10
+ import { createEventBus, globalEventBus, createMemoryEventTransport, createNatsJetStreamEventTransport } from '../src/event-bus.js';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // globalEventBus.emit notifies all subscribers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ test('globalEventBus.emit notifies all subscribers', () => {
17
+ // Use a local bus to avoid cross-test pollution from the module singleton
18
+ const bus = createEventBus();
19
+ const receivedA = [];
20
+ const receivedB = [];
21
+
22
+ bus.subscribe((e) => receivedA.push(e));
23
+ bus.subscribe((e) => receivedB.push(e));
24
+
25
+ bus.emit({ type: 'integration-test', value: 'ping' });
26
+
27
+ assert.equal(receivedA.length, 1);
28
+ assert.equal(receivedB.length, 1);
29
+ assert.equal(receivedA[0].value, 'ping');
30
+ assert.equal(receivedB[0].value, 'ping');
31
+ });
32
+
33
+ test('globalEventBus is a shared singleton — same object across imports', async () => {
34
+ // Re-import to confirm it's the same singleton reference
35
+ const { globalEventBus: bus2 } = await import('../src/event-bus.js');
36
+ assert.strictEqual(globalEventBus, bus2, 'globalEventBus must be a module-level singleton');
37
+ });
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // emitResourceChange includes correct fields
41
+ // ---------------------------------------------------------------------------
42
+
43
+ test('emitResourceChange includes kind, name, operation, type, and timestamp fields', () => {
44
+ const bus = createEventBus();
45
+ const events = [];
46
+ bus.subscribe((e) => events.push(e));
47
+
48
+ bus.emitResourceChange('Repository', 'my-repo', 'apply');
49
+
50
+ assert.equal(events.length, 1);
51
+ const ev = events[0];
52
+ assert.equal(ev.type, 'resource-change');
53
+ assert.equal(ev.kind, 'Repository');
54
+ assert.equal(ev.name, 'my-repo');
55
+ assert.equal(ev.operation, 'apply');
56
+ assert.ok(typeof ev.timestamp === 'string', 'timestamp must be a string');
57
+ assert.ok(new Date(ev.timestamp).getTime() > 0, 'timestamp must be a valid ISO date');
58
+ });
59
+
60
+ test('emitResourceChange for delete operation includes operation: delete', () => {
61
+ const bus = createEventBus();
62
+ const events = [];
63
+ bus.subscribe((e) => events.push(e));
64
+
65
+ bus.emitResourceChange('AgentStack', 'review-bot', 'delete');
66
+
67
+ const ev = events[0];
68
+ assert.equal(ev.operation, 'delete');
69
+ assert.equal(ev.kind, 'AgentStack');
70
+ assert.equal(ev.name, 'review-bot');
71
+ });
72
+
73
+ test('emitResourceChange broadcasts to all currently subscribed listeners', () => {
74
+ const bus = createEventBus();
75
+ const counts = [0, 0, 0];
76
+ bus.subscribe(() => counts[0]++);
77
+ bus.subscribe(() => counts[1]++);
78
+ bus.subscribe(() => counts[2]++);
79
+
80
+ bus.emitResourceChange('Pipeline', 'ci-pipeline', 'apply');
81
+
82
+ assert.deepEqual(counts, [1, 1, 1]);
83
+ });
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // unsubscribe stops notifications
87
+ // ---------------------------------------------------------------------------
88
+
89
+ test('unsubscribe stops further notifications to the removed listener', () => {
90
+ const bus = createEventBus();
91
+ const events = [];
92
+ const listener = (e) => events.push(e);
93
+
94
+ bus.subscribe(listener);
95
+ bus.emit({ type: 'before-unsub' });
96
+
97
+ bus.unsubscribe(listener);
98
+ bus.emit({ type: 'after-unsub' });
99
+
100
+ assert.equal(events.length, 1);
101
+ assert.equal(events[0].type, 'before-unsub');
102
+ });
103
+
104
+ test('unsubscribing one listener does not affect other listeners', () => {
105
+ const bus = createEventBus();
106
+ const eventsA = [];
107
+ const eventsB = [];
108
+ const listenerA = (e) => eventsA.push(e);
109
+ const listenerB = (e) => eventsB.push(e);
110
+
111
+ bus.subscribe(listenerA);
112
+ bus.subscribe(listenerB);
113
+
114
+ bus.emit({ type: 'first' });
115
+ bus.unsubscribe(listenerA);
116
+ bus.emit({ type: 'second' });
117
+
118
+ assert.equal(eventsA.length, 1, 'listenerA should only have received the first event');
119
+ assert.equal(eventsB.length, 2, 'listenerB should have received both events');
120
+ assert.equal(eventsB[1].type, 'second');
121
+ });
122
+
123
+ test('unsubscribing a listener that was never subscribed is a no-op', () => {
124
+ const bus = createEventBus();
125
+ const listener = () => {};
126
+ // Should not throw
127
+ assert.doesNotThrow(() => bus.unsubscribe(listener));
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Multiple subscribers receive same event
132
+ // ---------------------------------------------------------------------------
133
+
134
+ test('five subscribers all receive the same emitted event object', () => {
135
+ const bus = createEventBus();
136
+ const buckets = Array.from({ length: 5 }, () => []);
137
+ buckets.forEach((bucket) => bus.subscribe((e) => bucket.push(e)));
138
+
139
+ const event = { type: 'multi-fan-out', id: 'event-xyz' };
140
+ bus.emit(event);
141
+
142
+ for (const bucket of buckets) {
143
+ assert.equal(bucket.length, 1, 'each subscriber should receive exactly one event');
144
+ assert.strictEqual(bucket[0], event, 'each subscriber should receive the same event reference');
145
+ }
146
+ });
147
+
148
+ test('emit with zero subscribers does not throw and is a no-op', () => {
149
+ const bus = createEventBus();
150
+ assert.doesNotThrow(() => bus.emit({ type: 'nobody-home' }));
151
+ });
152
+
153
+ test('subscribers added after an emit do not receive previous events', () => {
154
+ const bus = createEventBus();
155
+ const events = [];
156
+
157
+ bus.emit({ type: 'before-subscribe' });
158
+ bus.subscribe((e) => events.push(e));
159
+
160
+ assert.equal(events.length, 0, 'late subscriber must not receive past events');
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Sequential emit ordering
165
+ // ---------------------------------------------------------------------------
166
+
167
+ test('emit delivers events to subscribers in subscription order', () => {
168
+ const bus = createEventBus();
169
+ const order = [];
170
+
171
+ bus.subscribe(() => order.push('first'));
172
+ bus.subscribe(() => order.push('second'));
173
+ bus.subscribe(() => order.push('third'));
174
+
175
+ bus.emit({ type: 'ordering-test' });
176
+
177
+ assert.deepEqual(order, ['first', 'second', 'third']);
178
+ });
179
+
180
+ test('multiple emits are each delivered to all subscribers independently', () => {
181
+ const bus = createEventBus();
182
+ const events = [];
183
+ bus.subscribe((e) => events.push(e.id));
184
+
185
+ bus.emit({ id: 1 });
186
+ bus.emit({ id: 2 });
187
+ bus.emit({ id: 3 });
188
+
189
+ assert.deepEqual(events, [1, 2, 3]);
190
+ });
191
+
192
+ test('event bus assigns durable ids and can replay from a cursor', () => {
193
+ const bus = createEventBus({ transport: createMemoryEventTransport() });
194
+ const first = bus.emit({ type: 'resource-change', name: 'one' });
195
+ const second = bus.emit({ type: 'resource-change', name: 'two' });
196
+ const replayed = bus.replaySince(first.id);
197
+
198
+ assert.ok(first.id, 'first event has a stable cursor id');
199
+ assert.ok(second.id, 'second event has a stable cursor id');
200
+ assert.deepEqual(replayed.map((event) => event.name), ['two']);
201
+ });
202
+
203
+ test('two buses sharing a transport receive the same durable event', () => {
204
+ const transport = createMemoryEventTransport();
205
+ const publisher = createEventBus({ transport });
206
+ const subscriber = createEventBus({ transport });
207
+ const received = [];
208
+
209
+ subscriber.subscribe((event) => received.push(event));
210
+ publisher.emitResourceChange('Repository', 'shared-repo', 'apply');
211
+
212
+ assert.equal(received.length, 1);
213
+ assert.equal(received[0].kind, 'Repository');
214
+ assert.equal(received[0].name, 'shared-repo');
215
+ });
216
+
217
+ test('listener failures do not block durable emit or healthy subscribers', () => {
218
+ const bus = createEventBus({ transport: createMemoryEventTransport() });
219
+ const received = [];
220
+ bus.subscribe(() => {
221
+ throw new Error('slow subscriber failed');
222
+ });
223
+ bus.subscribe((event) => received.push(event));
224
+
225
+ const emitted = bus.emit({ type: 'backpressure-test' });
226
+
227
+ assert.equal(emitted.type, 'backpressure-test');
228
+ assert.equal(received.length, 1);
229
+ assert.equal(bus.status().transport, 'memory');
230
+ });
231
+
232
+ test('required broker outage is exposed in event bus status', () => {
233
+ const bus = createEventBus({
234
+ transport: {
235
+ name: 'nats-jetstream',
236
+ available: false,
237
+ required: true,
238
+ status: () => ({ transport: 'nats-jetstream', status: 'error', reason: 'broker-unavailable' }),
239
+ publish: () => {
240
+ throw new Error('broker-unavailable');
241
+ },
242
+ subscribe: () => () => {},
243
+ replaySince: () => [],
244
+ },
245
+ });
246
+
247
+ assert.deepEqual(bus.status(), { transport: 'nats-jetstream', status: 'error', reason: 'broker-unavailable' });
248
+ assert.throws(() => bus.emit({ type: 'must-not-silently-fallback' }), /broker-unavailable/);
249
+ });
250
+
251
+ test('event transport status redacts broker credentials', () => {
252
+ const bus = createEventBus({
253
+ transport: createNatsJetStreamEventTransport({
254
+ required: true,
255
+ brokerClient: {
256
+ publish: () => {},
257
+ subscribe: () => () => {},
258
+ replaySince: () => [],
259
+ status: () => ({ transport: 'nats-jetstream', status: 'error', reason: 'connect nats://user:pass@nats:4222?token=secret-token', durable: true }),
260
+ },
261
+ }),
262
+ });
263
+
264
+ const serialized = JSON.stringify(bus.status());
265
+ assert.equal(bus.status().status, 'error');
266
+ assert.doesNotMatch(serialized, /user:pass|secret-token/);
267
+ assert.match(serialized, /\[redacted\]/);
268
+ });
269
+
270
+ test('required broker mode rejects publish while broker is not ready', () => {
271
+ const bus = createEventBus({
272
+ transport: createNatsJetStreamEventTransport({
273
+ required: true,
274
+ brokerClient: {
275
+ publish: () => {},
276
+ subscribe: () => () => {},
277
+ replaySince: () => [],
278
+ status: () => ({ transport: 'nats-jetstream', status: 'connecting', durable: true }),
279
+ },
280
+ }),
281
+ });
282
+
283
+ assert.throws(() => bus.emit({ type: 'must-wait-for-broker' }), /broker-connecting/);
284
+ });
285
+
286
+ test('required broker mode waits for async publish acknowledgement before local delivery', async () => {
287
+ const bus = createEventBus({
288
+ transport: createNatsJetStreamEventTransport({
289
+ required: true,
290
+ brokerClient: {
291
+ publish: () => Promise.reject(new Error('jetstream-down')),
292
+ subscribe: () => () => {},
293
+ replaySince: () => [],
294
+ status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true }),
295
+ },
296
+ }),
297
+ });
298
+ const received = [];
299
+ bus.subscribe((event) => received.push(event));
300
+
301
+ await assert.rejects(bus.emit({ type: 'must-be-durable' }), /jetstream-down/);
302
+
303
+ assert.equal(received.length, 0, 'local subscribers must not see required broker events that were not durably published');
304
+ assert.equal(bus.status().status, 'error');
305
+ assert.equal(bus.status().reason, 'jetstream-down');
306
+ });
307
+
308
+ test('broker echo does not duplicate events for local subscribers', () => {
309
+ let brokerListener;
310
+ const transport = createNatsJetStreamEventTransport({
311
+ subject: 'kradle.echo.events',
312
+ brokerClient: {
313
+ publish: (_subject, event) => {
314
+ brokerListener?.(event);
315
+ },
316
+ replaySince: () => [],
317
+ subscribe: (_subject, listener) => {
318
+ brokerListener = listener;
319
+ return () => { brokerListener = null; };
320
+ },
321
+ status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true }),
322
+ },
323
+ });
324
+ const bus = createEventBus({ transport });
325
+ const received = [];
326
+ bus.subscribe((event) => received.push(event));
327
+
328
+ const emitted = bus.emit({ type: 'broker-echo' });
329
+
330
+ assert.equal(received.length, 1);
331
+ assert.equal(received[0].id, emitted.id);
332
+ });
333
+
334
+ test('NATS JetStream transport delegates durable publish and replay to broker client', async () => {
335
+ const published = [];
336
+ const transport = createNatsJetStreamEventTransport({
337
+ subject: 'kradle.test.events',
338
+ brokerClient: {
339
+ publish: (subject, event) => {
340
+ published.push({ subject, event });
341
+ },
342
+ replaySince: (subject, cursor, limit) => [{ id: `${cursor}-next`, subject, limit }],
343
+ subscribe: () => () => {},
344
+ status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true }),
345
+ },
346
+ });
347
+ const bus = createEventBus({ transport });
348
+ const emitted = bus.emit({ type: 'brokered' });
349
+ const replayed = await bus.replaySince(emitted.id, 5);
350
+
351
+ assert.equal(published[0].subject, 'kradle.test.events');
352
+ assert.equal(published[0].event.id, emitted.id);
353
+ assert.deepEqual(replayed, [{ id: `${emitted.id}-next`, subject: 'kradle.test.events', limit: 5 }]);
354
+ assert.equal(bus.status().durable, true);
355
+ });