@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,253 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createGiteaService } from '../src/gitea-service.js';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Helpers — minimal fetch mocks
7
+ // ---------------------------------------------------------------------------
8
+
9
+ function makeFetch(responses) {
10
+ // responses: Map<string, { status, body, isText? }>
11
+ return async (url) => {
12
+ const match = [...responses.entries()].find(([pattern]) => url.includes(pattern));
13
+ if (!match) {
14
+ return {
15
+ ok: false,
16
+ status: 404,
17
+ json: async () => null,
18
+ text: async () => '',
19
+ };
20
+ }
21
+ const [, { status = 200, body, isText = false }] = match;
22
+ const ok = status >= 200 && status < 300;
23
+ return {
24
+ ok,
25
+ status,
26
+ json: async () => (isText ? body : body),
27
+ text: async () => (typeof body === 'string' ? body : JSON.stringify(body)),
28
+ };
29
+ };
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // 1. No URL → null
34
+ // ---------------------------------------------------------------------------
35
+
36
+ test('createGiteaService returns null when no URL is configured', () => {
37
+ const originalEnv = process.env.KRADLE_GITEA_HTTP_URL;
38
+ delete process.env.KRADLE_GITEA_HTTP_URL;
39
+ try {
40
+ const service = createGiteaService({});
41
+ assert.equal(service, null, 'should return null when giteaUrl is absent');
42
+ } finally {
43
+ if (originalEnv !== undefined) process.env.KRADLE_GITEA_HTTP_URL = originalEnv;
44
+ }
45
+ });
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // 2. URL provided → service object
49
+ // ---------------------------------------------------------------------------
50
+
51
+ test('createGiteaService returns a service object when a URL is provided', () => {
52
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl: makeFetch(new Map()) });
53
+ assert.ok(service !== null, 'should return a service');
54
+ assert.equal(service.available, true, 'available should be true');
55
+ assert.ok(typeof service.listTree === 'function', 'has listTree');
56
+ assert.ok(typeof service.getBlob === 'function', 'has getBlob');
57
+ assert.ok(typeof service.listBranches === 'function', 'has listBranches');
58
+ assert.ok(typeof service.getFileContent === 'function', 'has getFileContent');
59
+ assert.ok(typeof service.createRepository === 'function', 'has createRepository');
60
+ });
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // 3. listTree — calls correct Gitea endpoint
64
+ // ---------------------------------------------------------------------------
65
+
66
+ test('listTree calls the Gitea contents endpoint and maps type:dir to tree', async () => {
67
+ const captured = [];
68
+ const fetchImpl = async (url, opts) => {
69
+ captured.push(url);
70
+ return {
71
+ ok: true,
72
+ status: 200,
73
+ json: async () => [
74
+ { name: 'src', path: 'src', type: 'dir', size: 0, sha: 'abc' },
75
+ { name: 'README.md', path: 'README.md', type: 'file', size: 512, sha: 'def' },
76
+ ],
77
+ text: async () => '',
78
+ };
79
+ };
80
+
81
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
82
+ const entries = await service.listTree('myorg', 'myrepo', 'main', '');
83
+
84
+ assert.ok(captured[0].includes('/repos/myorg/myrepo/contents/'), 'called contents endpoint');
85
+ assert.ok(captured[0].includes('ref=main'), 'passed ref param');
86
+
87
+ assert.equal(entries.length, 2, 'should return 2 entries');
88
+ const dir = entries.find((e) => e.name === 'src');
89
+ assert.equal(dir.type, 'tree', 'type:dir should map to tree');
90
+ const file = entries.find((e) => e.name === 'README.md');
91
+ assert.equal(file.type, 'blob', 'type:file should map to blob');
92
+ assert.equal(file.size, 512, 'size is preserved');
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // 4. listTree — returns null for 404
97
+ // ---------------------------------------------------------------------------
98
+
99
+ test('listTree returns null when Gitea responds with 404', async () => {
100
+ const fetchImpl = async () => ({ ok: false, status: 404, json: async () => null, text: async () => '' });
101
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
102
+ const result = await service.listTree('org', 'missing-repo', 'main', '');
103
+ assert.equal(result, null, 'should return null for 404');
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // 5. getBlob — calls raw endpoint and returns text
108
+ // ---------------------------------------------------------------------------
109
+
110
+ test('getBlob calls the Gitea raw endpoint and returns file text', async () => {
111
+ const capturedUrls = [];
112
+ const expectedContent = 'console.log("hello");\n';
113
+ const fetchImpl = async (url) => {
114
+ capturedUrls.push(url);
115
+ return {
116
+ ok: true,
117
+ status: 200,
118
+ json: async () => null,
119
+ text: async () => expectedContent,
120
+ };
121
+ };
122
+
123
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
124
+ const content = await service.getBlob('myorg', 'myrepo', 'main', 'src/index.js');
125
+
126
+ assert.ok(capturedUrls[0].includes('/repos/myorg/myrepo/raw/'), 'called raw endpoint');
127
+ assert.ok(capturedUrls[0].includes('ref=main'), 'passed ref param');
128
+ assert.equal(content, expectedContent, 'returned raw file text');
129
+ });
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // 6. getBlob — returns null for 404
133
+ // ---------------------------------------------------------------------------
134
+
135
+ test('getBlob returns null when file is not found in Gitea', async () => {
136
+ const fetchImpl = async () => ({ ok: false, status: 404, json: async () => null, text: async () => '' });
137
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
138
+ const result = await service.getBlob('org', 'repo', 'main', 'nonexistent.js');
139
+ assert.equal(result, null, 'should return null for missing file');
140
+ });
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // 7. listBranches — maps branch response correctly
144
+ // ---------------------------------------------------------------------------
145
+
146
+ test('listBranches returns array with name, sha, protected fields', async () => {
147
+ const fetchImpl = async () => ({
148
+ ok: true,
149
+ status: 200,
150
+ json: async () => [
151
+ { name: 'main', commit: { id: 'sha-main' }, protected: true },
152
+ { name: 'dev', commit: { id: 'sha-dev' }, protected: false },
153
+ ],
154
+ text: async () => '',
155
+ });
156
+
157
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
158
+ const branches = await service.listBranches('myorg', 'myrepo');
159
+
160
+ assert.equal(branches.length, 2, 'two branches returned');
161
+ assert.equal(branches[0].name, 'main');
162
+ assert.equal(branches[0].sha, 'sha-main');
163
+ assert.equal(branches[0].protected, true);
164
+ assert.equal(branches[1].name, 'dev');
165
+ assert.equal(branches[1].protected, false);
166
+ });
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // 8. getFileContent — decodes base64 content
170
+ // ---------------------------------------------------------------------------
171
+
172
+ test('getFileContent decodes base64 content from Gitea response', async () => {
173
+ const originalText = 'export default function hello() {}\n';
174
+ const b64 = Buffer.from(originalText).toString('base64');
175
+
176
+ const fetchImpl = async () => ({
177
+ ok: true,
178
+ status: 200,
179
+ json: async () => ({
180
+ path: 'src/hello.js',
181
+ size: originalText.length,
182
+ sha: 'abc123',
183
+ content: b64,
184
+ last_commit_sha: 'commitsha',
185
+ }),
186
+ text: async () => '',
187
+ });
188
+
189
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
190
+ const file = await service.getFileContent('myorg', 'myrepo', 'main', 'src/hello.js');
191
+
192
+ assert.equal(file.content, originalText, 'content decoded from base64');
193
+ assert.equal(file.path, 'src/hello.js', 'path preserved');
194
+ assert.equal(file.sha, 'abc123', 'sha preserved');
195
+ assert.equal(file.lastCommit, 'commitsha', 'lastCommit mapped');
196
+ });
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // 9. Error handling — non-404 errors propagate
200
+ // ---------------------------------------------------------------------------
201
+
202
+ test('listTree throws when Gitea returns a non-404 error status', async () => {
203
+ const fetchImpl = async () => ({ ok: false, status: 500, json: async () => null, text: async () => '' });
204
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
205
+
206
+ await assert.rejects(
207
+ () => service.listTree('org', 'repo', 'main', ''),
208
+ /500/,
209
+ 'should throw on 500 error'
210
+ );
211
+ });
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // 10. createGiteaService picks up KRADLE_GITEA_HTTP_URL from env
215
+ // ---------------------------------------------------------------------------
216
+
217
+ test('createGiteaService reads KRADLE_GITEA_HTTP_URL from environment', () => {
218
+ const original = process.env.KRADLE_GITEA_HTTP_URL;
219
+ process.env.KRADLE_GITEA_HTTP_URL = 'http://gitea-from-env:3000';
220
+ try {
221
+ const fetchImpl = makeFetch(new Map());
222
+ const service = createGiteaService({ fetchImpl });
223
+ assert.ok(service !== null, 'should return a service when env var is set');
224
+ assert.equal(service.baseUrl, 'http://gitea-from-env:3000', 'baseUrl should match env var');
225
+ } finally {
226
+ if (original !== undefined) process.env.KRADLE_GITEA_HTTP_URL = original;
227
+ else delete process.env.KRADLE_GITEA_HTTP_URL;
228
+ }
229
+ });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // 11. listTree — subdirectory path is URL-encoded correctly
233
+ // ---------------------------------------------------------------------------
234
+
235
+ test('listTree encodes subdirectory path in the API URL', async () => {
236
+ const capturedUrls = [];
237
+ const fetchImpl = async (url) => {
238
+ capturedUrls.push(url);
239
+ return {
240
+ ok: true,
241
+ status: 200,
242
+ json: async () => [{ name: 'App.jsx', path: 'src/components/App.jsx', type: 'file', size: 100, sha: 'x' }],
243
+ text: async () => '',
244
+ };
245
+ };
246
+
247
+ const service = createGiteaService({ giteaUrl: 'http://localhost:3000', fetchImpl });
248
+ await service.listTree('org', 'repo', 'main', 'src/components');
249
+
250
+ // The path 'src/components' should appear in the URL (slashes may or may not be encoded)
251
+ assert.ok(capturedUrls[0].includes('src'), 'URL contains path segment src');
252
+ assert.ok(capturedUrls[0].includes('components'), 'URL contains path segment components');
253
+ });
@@ -0,0 +1,165 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createResource } from '../src/index.js';
4
+ import { createAgentAdapterController } from '../src/agent-adapter-controller.js';
5
+ import { createAgentStackController } from '../src/agent-stack-controller.js';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Slice C4 — Real health checks for adapters + MCP
9
+ //
10
+ // healthCheck() must perform a real HTTP fetch when spec.healthEndpoint is set.
11
+ // These tests use a mock fetch via dependency injection.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function makeAdapter(name, overrides = {}) {
15
+ return createResource('AgentAdapter', { name, namespace: 'kradle-org-default' }, {
16
+ organizationRef: 'default',
17
+ adapterType: 'subprocess',
18
+ transport: 'http',
19
+ capabilities: ['tool-use'],
20
+ ...overrides
21
+ });
22
+ }
23
+
24
+ function makeMcpServer(name, overrides = {}) {
25
+ return createResource('AgentMcpServer', { name, namespace: 'kradle-org-default' }, {
26
+ endpoint: 'http://localhost:9090/mcp',
27
+ ...overrides
28
+ });
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // C4.1: healthCheck with endpoint returns healthy when fetch succeeds
33
+ // ---------------------------------------------------------------------------
34
+
35
+ test('healthCheck with endpoint returns healthy when fetch succeeds', async () => {
36
+ const mockFetch = async (url, options) => {
37
+ return { ok: true, status: 200 };
38
+ };
39
+
40
+ const controller = createAgentAdapterController({ fetch: mockFetch });
41
+ const adapter = makeAdapter('healthy-adapter', { healthEndpoint: 'http://localhost:9090/health' });
42
+
43
+ const result = await controller.healthCheck(adapter);
44
+
45
+ assert.ok(result, 'healthCheck must return a result');
46
+ assert.equal(result.status, 'healthy', 'status must be "healthy" when fetch succeeds');
47
+ assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
48
+ assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
49
+ assert.ok(result.latencyMs >= 0, 'latencyMs must be non-negative');
50
+ assert.ok(!result.error, 'result must not have an error when healthy');
51
+ });
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // C4.2: healthCheck with endpoint returns unhealthy when fetch fails (non-ok)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ test('healthCheck with endpoint returns unhealthy when fetch returns non-ok status', async () => {
58
+ const mockFetch = async (url, options) => {
59
+ return { ok: false, status: 503 };
60
+ };
61
+
62
+ const controller = createAgentAdapterController({ fetch: mockFetch });
63
+ const adapter = makeAdapter('unhealthy-adapter', { healthEndpoint: 'http://localhost:9090/health' });
64
+
65
+ const result = await controller.healthCheck(adapter);
66
+
67
+ assert.ok(result, 'healthCheck must return a result');
68
+ assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch returns non-ok');
69
+ assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
70
+ assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
71
+ assert.ok(result.error, 'result must include an error message when unhealthy');
72
+ });
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // C4.3: healthCheck with endpoint returns unhealthy on network error / timeout
76
+ // ---------------------------------------------------------------------------
77
+
78
+ test('healthCheck with endpoint returns unhealthy when fetch throws (timeout/network)', async () => {
79
+ const mockFetch = async (url, options) => {
80
+ throw new Error('AbortError: The operation was aborted');
81
+ };
82
+
83
+ const controller = createAgentAdapterController({ fetch: mockFetch });
84
+ const adapter = makeAdapter('timeout-adapter', { healthEndpoint: 'http://localhost:9090/health' });
85
+
86
+ const result = await controller.healthCheck(adapter);
87
+
88
+ assert.ok(result, 'healthCheck must return a result');
89
+ assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch throws');
90
+ assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
91
+ assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
92
+ assert.ok(result.error, 'result must include an error message when fetch throws');
93
+ assert.ok(result.error.includes('AbortError') || result.error.includes('aborted') || result.error.length > 0, 'error message must be non-empty');
94
+ });
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // C4.4: healthCheck without endpoint returns unknown (existing behaviour preserved)
98
+ // ---------------------------------------------------------------------------
99
+
100
+ test('healthCheck without endpoint returns unknown with reason no-endpoint', async () => {
101
+ const controller = createAgentAdapterController();
102
+ const adapter = makeAdapter('no-endpoint-adapter');
103
+ // no healthEndpoint in spec
104
+
105
+ const result = await controller.healthCheck(adapter);
106
+
107
+ assert.ok(result, 'healthCheck must return a result');
108
+ assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint configured');
109
+ assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
110
+ assert.equal(result.adapterName, adapter.metadata.name, 'result must carry the adapter name');
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // C4.5: MCP server health check — healthy when endpoint fetch succeeds
115
+ // ---------------------------------------------------------------------------
116
+
117
+ test('AgentStackController MCP health check returns healthy when server endpoint responds', async () => {
118
+ const mockFetch = async (url, options) => {
119
+ return { ok: true, status: 200 };
120
+ };
121
+
122
+ const controller = createAgentStackController({ fetch: mockFetch });
123
+
124
+ const mcpServer = makeMcpServer('test-mcp', { endpoint: 'http://localhost:9090/mcp' });
125
+ const result = await controller.checkMcpHealth(mcpServer);
126
+
127
+ assert.ok(result, 'checkMcpHealth must return a result');
128
+ assert.equal(result.status, 'healthy', 'status must be "healthy" when fetch succeeds');
129
+ assert.ok(typeof result.latencyMs === 'number', 'result must include latencyMs');
130
+ });
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // C4.6: MCP server health check — unhealthy when fetch fails
134
+ // ---------------------------------------------------------------------------
135
+
136
+ test('AgentStackController MCP health check returns unhealthy when server fetch fails', async () => {
137
+ const mockFetch = async (url, options) => {
138
+ throw new Error('Connection refused');
139
+ };
140
+
141
+ const controller = createAgentStackController({ fetch: mockFetch });
142
+
143
+ const mcpServer = makeMcpServer('unreachable-mcp', { endpoint: 'http://localhost:9999/mcp' });
144
+ const result = await controller.checkMcpHealth(mcpServer);
145
+
146
+ assert.ok(result, 'checkMcpHealth must return a result');
147
+ assert.equal(result.status, 'unhealthy', 'status must be "unhealthy" when fetch throws');
148
+ assert.ok(result.error, 'result must include an error message');
149
+ });
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // C4.7: MCP server health check — unknown when no endpoint configured
153
+ // ---------------------------------------------------------------------------
154
+
155
+ test('AgentStackController MCP health check returns unknown when no endpoint configured', async () => {
156
+ const controller = createAgentStackController();
157
+
158
+ const mcpServer = makeMcpServer('no-endpoint-mcp', { endpoint: undefined });
159
+ delete mcpServer.spec.endpoint;
160
+ const result = await controller.checkMcpHealth(mcpServer);
161
+
162
+ assert.ok(result, 'checkMcpHealth must return a result');
163
+ assert.equal(result.status, 'unknown', 'status must be "unknown" when no endpoint configured');
164
+ assert.equal(result.reason, 'no-endpoint', 'reason must be "no-endpoint"');
165
+ });
@@ -0,0 +1,90 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { collectKradleHealthProbes } from '../src/health-probes.js';
4
+
5
+ test('collectKradleHealthProbes runs deep dependency probes without leaking secrets', async () => {
6
+ const requestedUrls = [];
7
+ const result = await collectKradleHealthProbes({
8
+ env: {
9
+ KRADLE_GITEA_HTTP_URL: 'https://gitea.internal/',
10
+ AGENT_MUX_URL: 'https://mux.internal',
11
+ KRADLE_CONTROLLER_URL: 'https://controller.internal',
12
+ ANTHROPIC_API_KEY: 'sk-ant-api03-redacted-test-key',
13
+ KRADLE_KUBECTL: 'kubectl-test',
14
+ },
15
+ fetchImpl: async (url) => {
16
+ requestedUrls.push(String(url));
17
+ return { ok: true, status: 200 };
18
+ },
19
+ execFileImpl: async (command, args) => {
20
+ assert.equal(command, 'kubectl-test');
21
+ assert.deepEqual(args, ['cluster-info']);
22
+ return { stdout: 'Kubernetes control plane is running', stderr: '' };
23
+ },
24
+ eventBus: {
25
+ status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true, subject: 'kradle.events' }),
26
+ },
27
+ timeoutMs: 25,
28
+ });
29
+
30
+ assert.deepEqual(requestedUrls.sort(), [
31
+ 'https://controller.internal/healthz',
32
+ 'https://gitea.internal/api/v1/version',
33
+ 'https://mux.internal/healthz',
34
+ ]);
35
+ assert.equal(result.kubernetes.status, 'ok');
36
+ assert.equal(result.gitea.status, 'ok');
37
+ assert.equal(result.agentMux.status, 'ok');
38
+ assert.equal(result.controller.status, 'ok');
39
+ assert.equal(result.assistant.status, 'ok');
40
+ assert.equal(result.eventTransport.status, 'ok');
41
+ assert.equal(result.eventTransport.transport, 'nats-jetstream');
42
+ assert.equal(result.assistant.reason, 'valid-format');
43
+ assert.doesNotMatch(JSON.stringify(result), /sk-ant-api03-redacted-test-key/);
44
+ });
45
+
46
+ test('collectKradleHealthProbes returns partial structured failures for unconfigured dependencies', async () => {
47
+ const result = await collectKradleHealthProbes({
48
+ env: {},
49
+ fetchImpl: async () => {
50
+ throw new Error('should not fetch unconfigured dependencies');
51
+ },
52
+ execFileImpl: async () => {
53
+ throw new Error('kubectl missing');
54
+ },
55
+ eventBus: {
56
+ status: () => ({ transport: 'memory', status: 'ok', durable: false }),
57
+ },
58
+ timeoutMs: 25,
59
+ });
60
+
61
+ assert.equal(result.gitea.status, 'not configured');
62
+ assert.equal(result.agentMux.status, 'not configured');
63
+ assert.equal(result.controller.status, 'not configured');
64
+ assert.equal(result.assistant.status, 'not configured');
65
+ assert.equal(result.kubernetes.status, 'error');
66
+ assert.match(result.kubernetes.error, /kubectl missing/);
67
+ });
68
+
69
+ test('collectKradleHealthProbes redacts dependency URL credentials and event transport errors', async () => {
70
+ const result = await collectKradleHealthProbes({
71
+ env: {
72
+ KRADLE_GITEA_HTTP_URL: 'https://user:pass@gitea.internal/?token=secret-token',
73
+ KRADLE_KUBECTL: 'kubectl-test',
74
+ },
75
+ fetchImpl: async () => {
76
+ throw new Error('failed https://user:pass@gitea.internal/?token=secret-token sk-ant-api03-secret');
77
+ },
78
+ execFileImpl: async () => ({ stdout: 'ok', stderr: '' }),
79
+ eventBus: {
80
+ status: () => ({ transport: 'nats-jetstream', status: 'error', reason: 'connect nats://user:pass@nats:4222?token=secret-token' }),
81
+ },
82
+ timeoutMs: 25,
83
+ });
84
+
85
+ const serialized = JSON.stringify(result);
86
+ assert.equal(result.gitea.status, 'error');
87
+ assert.equal(result.eventTransport.status, 'error');
88
+ assert.doesNotMatch(serialized, /user:pass|secret-token|sk-ant-api03-secret/);
89
+ assert.match(serialized, /\[redacted\]/);
90
+ });