@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,779 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { ControlPlane, GiteaGitService, GiteaRepositoryStore, RunnerScheduler, WebhookBus, createAdmissionPolicy, createAuthProviderConfig, buildAuthorizationRedirect, exchangeOAuthCodeForProfile, profileFromDelegatedHeaders, registerLoginProfile, createSessionCookie, parseSessionCookie, createInviteResource, createTeamResource, identityBackendSyncPlan, mapLoginProfileToKradleIdentity, createKradleApiController, createControllerUiModel, createKradleComponentCatalog, createKradleHandoffSummary, createKradleLifecycleSnapshot, createKradleMvpDemo, createPolicyRolloutModel, createResource, createKradleKubernetesReconciler, identityAccessReconciliationPlan, createKubernetesResourceClient, createKubernetesResourceGateway, createGiteaBackend, createGiteaRepositoryHosting, giteaIssueSyncPlan, githubProjectIssueSyncPlan, issueProjectRefs, issueRepositoryRefs, orgMemoryRepositoryName, createKradleGitOpsPlan, listResourceDefinitions, mapOidcIdentity, resourceSchemaForKind, resourceToYaml, runSmokeAssertions } from '../src/index.js';
4
+
5
+ const platform = mapOidcIdentity({ subject: 'platform', email: 'platform@example.com', groups: ['kradle:platform-engineers'] });
6
+ const developer = mapOidcIdentity({ subject: 'dev', email: 'dev@example.com', groups: ['kradle:developers'] });
7
+ const repoAdmin = mapOidcIdentity({ subject: 'admin', email: 'admin@example.com', groups: ['kradle:repo-admins'] });
8
+
9
+ function setOptionalEnv(name, value) {
10
+ if (value === undefined) delete process.env[name];
11
+ else process.env[name] = value;
12
+ }
13
+
14
+
15
+ test('Gitea backend maps Git hosting integrations to API-shaped calls', async () => {
16
+ const calls = [];
17
+ const backend = createGiteaBackend({ baseUrl: 'https://gitea.example.test', token: 'secret', fetchImpl: async (url, options) => {
18
+ calls.push({ url, options });
19
+ return { ok: true, status: 200, async json() { return { ok: true, url, method: options.method }; } };
20
+ } });
21
+
22
+ await backend.createOrganization({ name: 'kradle', fullName: 'Kradle' });
23
+ await backend.createRepository({ owner: 'kradle', name: 'app', private: true, defaultBranch: 'main' });
24
+ await backend.addUserSshKey({ title: 'alice', key: 'ssh-ed25519 USER' });
25
+ await backend.addDeployKey({ owner: 'kradle', repo: 'app', title: 'argocd', key: 'ssh-ed25519 AAAA' });
26
+ await backend.addCollaborator({ owner: 'kradle', repo: 'app', username: 'alice', permission: 'write' });
27
+ await backend.createTeam({ org: 'kradle', name: 'maintainers', permission: 'admin' });
28
+ await backend.createIssue({ owner: 'kradle', repo: 'app', title: 'Bug' });
29
+ await backend.createPullRequest({ owner: 'kradle', repo: 'app', title: 'Change', head: 'feature', base: 'main' });
30
+ await backend.protectBranch({ owner: 'kradle', repo: 'app', branch: 'main', statusChecks: ['ci/test'] });
31
+ await backend.createWebhook({ owner: 'kradle', repo: 'app', url: 'https://hooks.example.test/kradle', secret: 'hook-secret' });
32
+
33
+ assert.equal(backend.role, 'gitea-backend');
34
+ assert.deepEqual(calls.map((call) => call.options.method), ['POST', 'POST', 'POST', 'POST', 'PUT', 'POST', 'POST', 'POST', 'POST', 'POST']);
35
+ assert.ok(calls[0].url.endsWith('/api/v1/orgs'));
36
+ assert.ok(calls[1].url.endsWith('/api/v1/orgs/kradle/repos'));
37
+ assert.ok(calls[2].url.endsWith('/api/v1/user/keys'));
38
+ assert.ok(calls[3].url.endsWith('/api/v1/repos/kradle/app/keys'));
39
+ assert.ok(calls[4].url.endsWith('/api/v1/repos/kradle/app/collaborators/alice'));
40
+ assert.ok(calls.some((call) => call.url.endsWith('/api/v1/repos/kradle/app/issues')));
41
+ assert.ok(calls.some((call) => call.url.endsWith('/api/v1/repos/kradle/app/pulls')));
42
+ assert.ok(calls[8].url.endsWith('/api/v1/repos/kradle/app/branch_protections'));
43
+ assert.ok(JSON.parse(calls[9].options.body).config.secret === 'hook-secret');
44
+ });
45
+
46
+ test('Argo CD GitOps plan creates an automated Kradle Application', () => {
47
+ const plan = createKradleGitOpsPlan({ repoURL: 'https://gitea.example.test/kradle/platform-config.git', namespace: 'kradle-system' });
48
+ assert.equal(plan.engine, 'argocd');
49
+ assert.equal(plan.application.apiVersion, 'argoproj.io/v1alpha1');
50
+ assert.equal(plan.application.kind, 'Application');
51
+ assert.equal(plan.application.spec.source.repoURL, 'https://gitea.example.test/kradle/platform-config.git');
52
+ assert.equal(plan.application.spec.destination.namespace, 'kradle-system');
53
+ assert.equal(plan.application.spec.syncPolicy.automated.prune, true);
54
+ assert.equal(plan.application.spec.syncPolicy.automated.selfHeal, true);
55
+ assert.ok(plan.requiredClusterResources.includes('Application.argoproj.io'));
56
+ });
57
+
58
+ test('Gitea repository hosting plan includes Argo CD deploy key and webhook integration', () => {
59
+ const hosting = createGiteaRepositoryHosting({ namespace: 'kradle-system', repository: 'platform-config' });
60
+ assert.equal(hosting.backend, 'gitea');
61
+ assert.match(hosting.httpUrl, /platform-config\.git$/);
62
+ assert.ok(hosting.integrationPlan.operations.some((step) => step.action === 'addDeployKey' && step.title === 'kradle-argocd'));
63
+ assert.ok(hosting.integrationPlan.operations.some((step) => step.action === 'createWebhook'));
64
+ assert.equal(hosting.forgeRecords.issues, 'Gitea /repos/kradle/_kradle-system_/issues');
65
+ assert.equal(hosting.issueSync.repo, '_kradle-system_');
66
+ assert.ok(hosting.issueSync.actions.some((step) => step.action === 'ensureOrgMemoryRepository'));
67
+ });
68
+ test('resource catalog exposes all ontology kinds and storage boundaries', () => {
69
+ const definitions = listResourceDefinitions();
70
+ assert.equal(definitions.length, 98);
71
+ assert.deepEqual(definitions.filter((definition) => definition.storage === 'etcd').map((definition) => definition.kind), ['Organization', 'OrgNamespaceBinding', 'User', 'Team', 'Invite', 'IdentityMapping', 'AuthProvider', 'Repository', 'SSHKey', 'RepositoryPermission', 'WebhookSubscription', 'RefPolicy', 'BranchProtection', 'PolicyProfile', 'PolicyTemplate', 'PolicyBinding', 'PolicyExceptionRequest', 'View', 'Selector', 'RunnerPool', 'AgentStack', 'AgentPersona', 'AgentSoul', 'AgentAppearance', 'AgentVoiceProfile', 'AgentDefinition', 'AgentSubagent', 'AgentToolProfile', 'AgentMcpServer', 'AgentSkill', 'AgentTriggerRule', 'AgentContextLabel', 'KradleWorkspacePolicy', 'AgentServiceAccount', 'AgentRoleBinding', 'AgentSecretGrant', 'AgentConfigGrant', 'KradleWorkspace', 'AgentAdapter', 'AgentTransportBinding', 'AgentProviderConfig', 'KradleProject', 'AgentGatewayConfig', 'AgentMemoryRepository', 'AgentMemorySource', 'AgentMemoryOntology', 'AgentMemoryAssociation', 'GitProvider', 'CiProvider', 'IssueTrackerProvider', 'AppHostingProvider', 'ArtifactRegistryProvider', 'ExternalBackendBinding', 'ExternalBackendSyncPolicy', 'ExternalProviderCapabilityManifest', 'ArtifactRegistry', 'ArtifactFeed', 'ArtifactAccessPolicy', 'KradleInferenceService', 'KradleServingRuntime', 'ExternalWebhookConfig', 'JitsiMeetProvider', 'JitsiMeetingTemplate', 'KradleModelRoute', 'KradleVirtualModel']);
72
+ assert.deepEqual(definitions.filter((definition) => definition.storage === 'postgres').map((definition) => definition.kind), ['PullRequest', 'Issue', 'Review', 'Pipeline', 'Job', 'WebhookDelivery', 'AgentDispatchRun', 'AgentDispatchAttempt', 'AgentSession', 'AgentContextBundle', 'KradleArtifact', 'AgentApproval', 'AgentTriggerExecution', 'AgentCapabilityRequirement', 'WorkItemSessionLink', 'WorkItemWorkspaceLink', 'AgentSessionTranscript', 'AgentSessionAttachment', 'KradleWorkspaceRuntime', 'AgentMemorySnapshot', 'AgentMemoryQuery', 'AgentMemoryUpdate', 'AgentRunMemoryImport', 'ExternalWebhookDelivery', 'ExternalSyncEvent', 'ExternalSyncState', 'ExternalWriteIntent', 'ExternalSyncConflict', 'ExternalObjectLink', 'ArtifactVersion', 'ArtifactDownload', 'JitsiMeeting', 'JitsiRecording']);
73
+ assert.equal(resourceSchemaForKind('Organization').plural, 'organizations');
74
+ assert.equal(resourceSchemaForKind('User').plural, 'users');
75
+ assert.equal(resourceSchemaForKind('Team').plural, 'teams');
76
+ assert.equal(resourceSchemaForKind('Invite').plural, 'invites');
77
+ assert.equal(resourceSchemaForKind('IdentityMapping').plural, 'identitymappings');
78
+ assert.equal(resourceSchemaForKind('AuthProvider').plural, 'authproviders');
79
+ assert.equal(resourceSchemaForKind('SSHKey').plural, 'sshkeys');
80
+ assert.equal(resourceSchemaForKind('RepositoryPermission').plural, 'repositorypermissions');
81
+ assert.equal(resourceSchemaForKind('KradleProject').plural, 'kradleprojects');
82
+ const schema = resourceSchemaForKind('PullRequest');
83
+ assert.deepEqual(schema.required.spec, ['organizationRef', 'repository', 'title']);
84
+ assert.match(resourceToYaml(createResource('Repository', { name: 'yaml-demo' }, { organizationRef: 'default', visibility: 'private' })), /kind: Repository/);
85
+ assert.match(resourceToYaml(createResource('SSHKey', { name: 'alice-key' }, { organizationRef: 'default', scope: 'user', key: 'ssh-ed25519 AAAA' })), /kind: SSHKey/);
86
+ assert.match(resourceToYaml(createResource('KradleProject', { name: 'triage' }, { organizationRef: 'default', displayName: 'Triage', repositories: ['app'] })), /kind: KradleProject/);
87
+ assert.match(resourceToYaml({ apiVersion: 'core.oam.dev/v1beta1', kind: 'Application', spec: { components: [{ name: 'web', properties: { image: 'kradle/mvp-model:0.1.0' }, traits: [{ type: 'scaler', properties: { replicas: 1 } }] }] } }), /components:\n - name: web\n properties:\n image: kradle\/mvp-model:0.1.0\n traits:\n - type: scaler/);
88
+ });
89
+
90
+
91
+
92
+ test('issue project and repository associations are normalized for scoped views', () => {
93
+ const issue = createResource('Issue', { name: 'bug-1', annotations: { 'kradle.a5c.ai/repositories': 'app, docs', 'kradle.a5c.ai/projects': 'planning' } }, {
94
+ organizationRef: 'default',
95
+ title: 'Bug',
96
+ repositoryRefs: [{ name: 'api' }, { repository: 'ui' }],
97
+ repositories: ['worker'],
98
+ projectRef: { name: 'release' },
99
+ projects: ['roadmap']
100
+ });
101
+ assert.deepEqual(issueRepositoryRefs(issue).sort(), ['api', 'app', 'docs', 'ui', 'worker']);
102
+ assert.deepEqual(issueProjectRefs(issue).sort(), ['planning', 'release', 'roadmap']);
103
+
104
+ const gitea = giteaIssueSyncPlan({ org: 'default', project: 'release', issue, repositories: issueRepositoryRefs(issue) });
105
+ assert.equal(orgMemoryRepositoryName('default'), '_default_');
106
+ assert.equal(gitea.repo, '_default_');
107
+ assert.deepEqual(gitea.metadataKeys, ['kradle.a5c.ai/project', 'kradle.a5c.ai/repositories']);
108
+ assert.ok(gitea.actions.some((step) => step.action === 'writeIssueRepositoryMetadata'));
109
+
110
+ const github = githubProjectIssueSyncPlan({ org: 'default', project: 'release', issue, repositories: issueRepositoryRefs(issue) });
111
+ assert.equal(github.project, 'release');
112
+ assert.ok(github.actions.includes('syncProjectItem'));
113
+ });
114
+
115
+
116
+ test('Kradle auth resources map sign-in, teams, invites, and repository identities', () => {
117
+ const config = createAuthProviderConfig({
118
+ KRADLE_AUTH_GITHUB_ENABLED: 'true',
119
+ KRADLE_AUTH_GITHUB_CLIENT_ID: 'gh-client',
120
+ KRADLE_AUTH_GITHUB_CLIENT_SECRET: 'secret',
121
+ KRADLE_AUTH_SSO_ENABLED: 'true',
122
+ KRADLE_AUTH_SSO_PROVIDER_NAME: 'Company SSO',
123
+ KRADLE_AUTH_SSO_CLIENT_ID: 'sso-client',
124
+ KRADLE_AUTH_SSO_CLIENT_SECRET: 'secret',
125
+ KRADLE_AUTH_SSO_AUTHORIZATION_URL: 'https://idp.example.test/authorize',
126
+ KRADLE_AUTH_SSO_TOKEN_URL: 'https://idp.example.test/token',
127
+ KRADLE_AUTH_SSO_USERINFO_URL: 'https://idp.example.test/userinfo',
128
+ KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED: 'true'
129
+ });
130
+ const redirect = buildAuthorizationRedirect({ provider: config.providers.sso, requestUrl: 'https://kradle.example.test/login', state: 'state' });
131
+ const mapped = mapLoginProfileToKradleIdentity({ provider: 'sso', subject: 'user-1', email: 'alice@example.com', displayName: 'Alice', username: 'alice', teams: ['maintainers'], admin: true, namespace: 'kradle-test' });
132
+ const team = createTeamResource({ name: 'maintainers', members: ['alice'], repositoryGrants: [{ repository: 'app', permission: 'admin' }], namespace: 'kradle-test' });
133
+ const invite = createInviteResource({ email: 'bob@example.com', teams: ['maintainers'], namespace: 'kradle-test' });
134
+ const plan = identityBackendSyncPlan({ users: [mapped.user], teams: [team], invites: [invite], mappings: [mapped.mapping], permissions: [createResource('RepositoryPermission', { name: 'app-alice', namespace: 'kradle-test' }, { organizationRef: 'default', repository: 'app', subject: 'alice', subjectKind: 'user', permission: 'admin' })] });
135
+
136
+ assert.equal(config.providers.github.enabled, true);
137
+ assert.equal(config.providers.sso.label, 'Company SSO');
138
+ assert.equal(config.delegatedIdentity.enabled, true);
139
+ assert.ok(redirect.url.includes('client_id=sso-client'));
140
+ assert.equal(mapped.user.kind, 'User');
141
+ assert.equal(mapped.mapping.spec.repositoryIdentity.username, 'alice');
142
+ assert.equal(team.kind, 'Team');
143
+ assert.equal(invite.kind, 'Invite');
144
+ assert.deepEqual(plan.mappings.map((entry) => entry.action), ['link-identity']);
145
+ assert.deepEqual(plan.permissions.map((entry) => entry.action), ['sync-repository-permission']);
146
+ });
147
+
148
+ test('session cookies parse into the signed-in Kradle user', () => {
149
+ const config = createAuthProviderConfig({ KRADLE_AUTH_COOKIE_NAME: 'kradle_session' });
150
+ const cookie = createSessionCookie(config, { provider: 'sso', subject: 'alice-subject', username: 'alice' });
151
+ const cookieValue = cookie.match(/kradle_session=([^;]+)/)?.[1];
152
+
153
+ assert.deepEqual(parseSessionCookie(config, cookieValue), {
154
+ cookieName: 'kradle_session',
155
+ provider: 'sso',
156
+ subject: 'alice-subject',
157
+ user: 'alice'
158
+ });
159
+ assert.equal(parseSessionCookie(config, 'not-json'), null);
160
+ });
161
+
162
+ test('OAuth callbacks and delegated identity auto-register Kradle users and mappings', async () => {
163
+ const config = createAuthProviderConfig({
164
+ KRADLE_AUTH_GITHUB_ENABLED: 'true',
165
+ KRADLE_AUTH_GITHUB_CLIENT_ID: 'gh-client',
166
+ KRADLE_AUTH_GITHUB_CLIENT_SECRET: 'gh-secret',
167
+ KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED: 'true'
168
+ });
169
+ const calls = [];
170
+ const profile = await exchangeOAuthCodeForProfile({
171
+ provider: config.providers.github,
172
+ code: 'oauth-code',
173
+ requestUrl: 'https://kradle.example.test/api/auth/callback/github',
174
+ fetchImpl: async (url, options = {}) => {
175
+ calls.push({ url, options });
176
+ if (String(url).includes('access_token')) return { ok: true, status: 200, async json() { return { access_token: 'token' }; } };
177
+ return { ok: true, status: 200, async json() { return { id: 42, login: 'alice', email: 'alice@example.com', name: 'Alice' }; } };
178
+ }
179
+ });
180
+ const applied = [];
181
+ const registration = await registerLoginProfile({
182
+ namespace: 'kradle-test',
183
+ profile,
184
+ controller: { async applyResource(resource) { applied.push(resource); return { operation: 'apply', resource }; } }
185
+ });
186
+ const delegated = profileFromDelegatedHeaders({ 'x-forwarded-user': 'bob', 'x-forwarded-email': 'bob@example.com', 'x-forwarded-groups': 'kradle:repo-admins,team:release' }, config);
187
+
188
+ assert.equal(profile.username, 'alice');
189
+ assert.equal(calls.length, 2);
190
+ assert.deepEqual(applied.map((resource) => resource.kind), ['User', 'IdentityMapping']);
191
+ assert.equal(registration.mapping.spec.repositoryIdentity.username, 'alice');
192
+ assert.equal(delegated.admin, true);
193
+ assert.equal(delegated.email, 'bob@example.com');
194
+ assert.equal(delegated.delegatedIdentitySource, 'proxy-header');
195
+ });
196
+
197
+
198
+ test('OAuth callback route completes fake SSO exchange and registers Kradle identity', async () => {
199
+ const previous = {
200
+ enabled: process.env.KRADLE_AUTH_SSO_ENABLED,
201
+ providerName: process.env.KRADLE_AUTH_SSO_PROVIDER_NAME,
202
+ clientId: process.env.KRADLE_AUTH_SSO_CLIENT_ID,
203
+ clientSecret: process.env.KRADLE_AUTH_SSO_CLIENT_SECRET,
204
+ authorizationUrl: process.env.KRADLE_AUTH_SSO_AUTHORIZATION_URL,
205
+ tokenUrl: process.env.KRADLE_AUTH_SSO_TOKEN_URL,
206
+ userInfoUrl: process.env.KRADLE_AUTH_SSO_USERINFO_URL,
207
+ scopes: process.env.KRADLE_AUTH_SSO_SCOPES,
208
+ namespace: process.env.KRADLE_NAMESPACE
209
+ };
210
+ Object.assign(process.env, {
211
+ KRADLE_AUTH_SSO_ENABLED: 'true',
212
+ KRADLE_AUTH_SSO_PROVIDER_NAME: 'Test Workspace SSO',
213
+ KRADLE_AUTH_SSO_CLIENT_ID: 'fake-client',
214
+ KRADLE_AUTH_SSO_CLIENT_SECRET: 'fake-secret',
215
+ KRADLE_AUTH_SSO_AUTHORIZATION_URL: 'https://idp.example.test/authorize',
216
+ KRADLE_AUTH_SSO_TOKEN_URL: 'https://idp.example.test/token',
217
+ KRADLE_AUTH_SSO_USERINFO_URL: 'https://idp.example.test/userinfo',
218
+ KRADLE_AUTH_SSO_SCOPES: 'openid profile email groups',
219
+ KRADLE_NAMESPACE: 'kradle-test'
220
+ });
221
+ const applied = [];
222
+ const fetchCalls = [];
223
+ const controller = { async applyResource(resource) { applied.push(resource); return { operation: 'apply', resource }; } };
224
+ const fetchImpl = async (url, options = {}) => {
225
+ fetchCalls.push({ url: String(url), options });
226
+ if (String(url).endsWith('/token')) {
227
+ const body = new URLSearchParams(String(options.body));
228
+ assert.equal(body.get('code'), 'fake-code');
229
+ assert.equal(body.get('client_id'), 'fake-client');
230
+ assert.equal(body.get('client_secret'), 'fake-secret');
231
+ assert.equal(body.get('redirect_uri'), 'https://kradle.example.test/api/auth/callback/sso');
232
+ return { ok: true, status: 200, async json() { return { access_token: 'fake-access-token' }; } };
233
+ }
234
+ assert.equal(options.headers.Authorization, 'Bearer fake-access-token');
235
+ return { ok: true, status: 200, async json() { return { sub: 'user-123', email: 'route@example.test', preferred_username: 'route-alice', name: 'Route Alice', groups: ['kradle:repo-admins', 'team:platform'] }; } };
236
+ };
237
+
238
+ try {
239
+ const { createOAuthCallbackHandler } = await import(`../../web/app/api/auth/callback/[provider]/route.js?test=${Date.now()}`);
240
+ const handler = createOAuthCallbackHandler({ controller, fetchImpl });
241
+ const response = await handler(new Request('https://kradle.example.test/api/auth/callback/sso?code=fake-code&state=state'), { params: { provider: 'sso' } });
242
+
243
+ assert.equal(response.status, 302);
244
+ assert.equal(response.headers.get('location'), '/orgs/default/people');
245
+ assert.equal(response.headers.get('x-kradle-user'), 'route-alice');
246
+ assert.match(response.headers.get('set-cookie'), /kradle_session=/);
247
+ assert.deepEqual(fetchCalls.map((call) => call.url), ['https://idp.example.test/token', 'https://idp.example.test/userinfo']);
248
+ assert.deepEqual(applied.map((resource) => resource.kind), ['User', 'IdentityMapping']);
249
+ assert.equal(applied[0].metadata.name, 'route-alice');
250
+ assert.equal(applied[0].spec.admin, true);
251
+ assert.equal(applied[1].spec.provider, 'sso');
252
+ assert.equal(applied[1].spec.repositoryIdentity.username, 'route-alice');
253
+ } finally {
254
+ setOptionalEnv('KRADLE_AUTH_SSO_ENABLED', previous.enabled);
255
+ setOptionalEnv('KRADLE_AUTH_SSO_PROVIDER_NAME', previous.providerName);
256
+ setOptionalEnv('KRADLE_AUTH_SSO_CLIENT_ID', previous.clientId);
257
+ setOptionalEnv('KRADLE_AUTH_SSO_CLIENT_SECRET', previous.clientSecret);
258
+ setOptionalEnv('KRADLE_AUTH_SSO_AUTHORIZATION_URL', previous.authorizationUrl);
259
+ setOptionalEnv('KRADLE_AUTH_SSO_TOKEN_URL', previous.tokenUrl);
260
+ setOptionalEnv('KRADLE_AUTH_SSO_USERINFO_URL', previous.userInfoUrl);
261
+ setOptionalEnv('KRADLE_AUTH_SSO_SCOPES', previous.scopes);
262
+ setOptionalEnv('KRADLE_NAMESPACE', previous.namespace);
263
+ }
264
+ });
265
+
266
+ test('Delegated identity supports localhost development fallback without proxy headers', () => {
267
+ const config = createAuthProviderConfig({
268
+ KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED: 'true',
269
+ KRADLE_AUTH_DELEGATED_LOCAL_USER: 'Dev Alice',
270
+ KRADLE_AUTH_DELEGATED_LOCAL_EMAIL: 'alice@example.test',
271
+ KRADLE_AUTH_DELEGATED_LOCAL_GROUPS: 'kradle:repo-admins,team:local'
272
+ });
273
+
274
+ const profile = profileFromDelegatedHeaders({}, config, { requestUrl: 'http://localhost:3000/api/auth/delegated' });
275
+
276
+ assert.equal(profile.username, 'dev-alice');
277
+ assert.equal(profile.email, 'alice@example.test');
278
+ assert.equal(profile.admin, true);
279
+ assert.equal(profile.delegatedIdentitySource, 'local-development');
280
+ assert.deepEqual(profile.groups, ['kradle:repo-admins', 'team:local']);
281
+ });
282
+
283
+ test('Delegated identity localhost fallback stays disabled for production unless explicitly enabled', () => {
284
+ const config = createAuthProviderConfig({ KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED: 'true', NODE_ENV: 'production' });
285
+
286
+ assert.equal(config.delegatedIdentity.localDevelopment.enabled, false);
287
+ assert.throws(
288
+ () => profileFromDelegatedHeaders({}, config, { requestUrl: 'https://kradle.example.test/api/auth/delegated' }),
289
+ /Delegated identity header x-forwarded-user is missing/
290
+ );
291
+ assert.throws(
292
+ () => profileFromDelegatedHeaders({}, config, { requestUrl: 'http://localhost:3000/api/auth/delegated' }),
293
+ /Delegated identity header x-forwarded-user is missing/
294
+ );
295
+
296
+ const explicitConfig = createAuthProviderConfig({
297
+ KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED: 'true',
298
+ KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT: 'true',
299
+ NODE_ENV: 'production'
300
+ });
301
+ const profile = profileFromDelegatedHeaders({}, explicitConfig, { requestUrl: 'http://localhost:3000/api/auth/delegated' });
302
+ assert.equal(profile.username, 'local-developer');
303
+ assert.equal(profile.delegatedIdentitySource, 'local-development');
304
+ const boundAddressProfile = profileFromDelegatedHeaders({}, explicitConfig, { requestUrl: 'http://0.0.0.0:8080/api/auth/delegated' });
305
+ assert.equal(boundAddressProfile.username, 'local-developer');
306
+ });
307
+
308
+ test('Delegated identity route redirects localhost fallback even when Kubernetes registration is unavailable', async () => {
309
+ const previous = {
310
+ delegated: process.env.KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED,
311
+ kubectl: process.env.KRADLE_KUBECTL,
312
+ timeout: process.env.KRADLE_KUBECTL_TIMEOUT_MS
313
+ };
314
+ process.env.KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED = 'true';
315
+ process.env.KRADLE_KUBECTL = 'kradle-missing-kubectl';
316
+ process.env.KRADLE_KUBECTL_TIMEOUT_MS = '50';
317
+ try {
318
+ const { GET } = await import(`../../web/app/api/auth/delegated/route.js?test=${Date.now()}`);
319
+ const response = await GET(new Request('http://localhost:3000/api/auth/delegated?user=Route%20Alice&email=route@example.test&groups=kradle:developers'));
320
+
321
+ assert.equal(response.status, 302);
322
+ assert.equal(response.headers.get('location'), '/orgs/default/people');
323
+ assert.equal(response.headers.get('x-kradle-user'), 'route-alice');
324
+ assert.match(response.headers.get('set-cookie'), /kradle_session=/);
325
+ } finally {
326
+ setOptionalEnv('KRADLE_AUTH_DELEGATED_IDENTITY_ENABLED', previous.delegated);
327
+ setOptionalEnv('KRADLE_KUBECTL', previous.kubectl);
328
+ setOptionalEnv('KRADLE_KUBECTL_TIMEOUT_MS', previous.timeout);
329
+ }
330
+ });
331
+
332
+
333
+
334
+ test('controller UI model keeps namespace-scoped resources visible for their org', () => {
335
+ const model = createControllerUiModel({
336
+ source: 'kubernetes',
337
+ namespace: 'kradle-system',
338
+ generatedAt: 'test-time',
339
+ kubectl: { available: true, context: 'kind-kradle', errors: [] },
340
+ crds: [],
341
+ resources: {
342
+ Organization: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'kradle-system' }, spec: { slug: 'default', namespaceName: 'kradle-org-default' } }],
343
+ RunnerPool: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'RunnerPool', metadata: { name: 'default', namespace: 'kradle-org-default' }, spec: { image: 'ubuntu:24.04' } }]
344
+ },
345
+ events: [],
346
+ permissions: [],
347
+ storage: {},
348
+ commands: []
349
+ }, { organization: 'default' });
350
+
351
+ assert.equal(model.metrics.runnerPools, 1);
352
+ assert.deepEqual(model.resources.find((resource) => resource.kind === 'RunnerPool').names, ['default']);
353
+ });
354
+
355
+ test('Kradle delivery resources surface through controller UI model', () => {
356
+ const model = createControllerUiModel({
357
+ source: 'kubernetes',
358
+ namespace: 'kradle-system',
359
+ generatedAt: 'test-time',
360
+ kubectl: { available: true, context: 'kind-kradle', errors: [] },
361
+ crds: [{ spec: { group: 'core.oam.dev', names: { plural: 'applications' } } }],
362
+ resources: {
363
+ Organization: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'kradle-system' }, spec: { slug: 'default', namespaceName: 'kradle-org-default', displayName: 'Default org' } }],
364
+ KubeVelaApplication: [{ apiVersion: 'core.oam.dev/v1beta1', kind: 'Application', metadata: { name: 'app', namespace: 'kradle-org-default', labels: { 'kradle.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default', components: [{ name: 'web', type: 'webservice' }], workflow: { steps: [{ name: 'deploy', type: 'deploy' }] } }, status: { status: 'running', appliedResources: [{ apiVersion: 'apps/v1', kind: 'Deployment', namespace: 'kradle-org-default', name: 'web' }], workflow: { status: 'succeeded', finished: true, appRevision: 'app-v1', steps: [{ name: 'deploy', type: 'deploy', phase: 'succeeded' }] }, services: [{ name: 'web', namespace: 'kradle-system', healthy: true, message: 'Ready:1/1', workloadDefinition: { apiVersion: 'apps/v1', kind: 'Deployment' } }] } }],
365
+ KubeVelaApplicationRevision: [{ metadata: { name: 'app-v1', labels: { 'app.oam.dev/name': 'app', 'kradle.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default' }, status: { succeeded: true } }],
366
+ KubeVelaComponentDefinition: [{ metadata: { name: 'webservice' } }],
367
+ KubeVelaWorkloadDefinition: [{ metadata: { name: 'deployments.apps' } }],
368
+ KubeVelaTraitDefinition: [{ metadata: { name: 'ingress' } }],
369
+ KubeVelaScopeDefinition: [{ metadata: { name: 'healthscopes.core.oam.dev' } }],
370
+ KubeVelaPolicyDefinition: [],
371
+ KubeVelaPolicy: [{ metadata: { name: 'topology', labels: { 'kradle.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default', type: 'topology' } }],
372
+ KubeVelaWorkflowStepDefinition: [{ metadata: { name: 'deploy' } }],
373
+ KubeVelaWorkflow: [{ metadata: { name: 'app', labels: { 'kradle.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default' }, status: { phase: 'running' } }],
374
+ KubeVelaResourceTracker: [{ metadata: { name: 'app-v1-kradle-system', labels: { 'app.oam.dev/name': 'app', 'kradle.a5c.ai/org': 'default' } }, spec: { organizationRef: 'default', type: 'versioned' } }]
375
+ },
376
+ events: [],
377
+ permissions: [],
378
+ storage: {},
379
+ commands: []
380
+ });
381
+ assert.equal(model.delivery.installed, true);
382
+ assert.equal(model.delivery.counts.applications, 1);
383
+ assert.deepEqual(model.delivery.capabilityCatalog.components, ['webservice']);
384
+ assert.deepEqual(model.delivery.capabilityCatalog.workloads, ['deployments.apps']);
385
+ assert.deepEqual(model.delivery.capabilityCatalog.scopes, ['healthscopes.core.oam.dev']);
386
+ assert.equal(model.delivery.specVersion, 'v0.3.0');
387
+ assert.equal(model.delivery.counts.releases, 1);
388
+ assert.equal(model.delivery.counts.managedResources, 1);
389
+ assert.equal(model.delivery.applications[0].healthy, true);
390
+ assert.deepEqual(model.delivery.applications[0].appliedResources.map((resource) => `${resource.kind}/${resource.namespace}/${resource.name}`), ['Deployment/kradle-org-default/web']);
391
+ assert.equal(model.delivery.applications[0].workflow.status, 'succeeded');
392
+ assert.deepEqual(model.delivery.runtime.managedResources.map((tracker) => tracker.name), ['app-v1-kradle-system']);
393
+ });
394
+
395
+ test('control plane keeps CRD config in etcd and aggregated records in Postgres', () => {
396
+ const controlPlane = new ControlPlane();
397
+ controlPlane.create(createResource('Repository', { name: 'app' }, { organizationRef: 'default', visibility: 'private' }), repoAdmin);
398
+ controlPlane.create(createResource('PullRequest', { name: 'pr-1' }, { organizationRef: 'default', repository: 'app', title: 'Improve platform routing' }), developer);
399
+ const report = controlPlane.storageReport();
400
+ assert.deepEqual(report.etcd, ['Repository']);
401
+ assert.deepEqual(report.postgres, ['PullRequest']);
402
+ });
403
+
404
+
405
+ test('API controller is an application facade over the Kubernetes resource gateway', async () => {
406
+ const calls = [];
407
+ const repository = createResource('Repository', { name: 'app', namespace: 'kradle-test' }, { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' });
408
+ const resourceGateway = {
409
+ role: 'kubernetes-resource-gateway',
410
+ namespace: 'kradle-test',
411
+ resourceDefinitions: listResourceDefinitions(),
412
+ async snapshot() {
413
+ calls.push(['snapshot']);
414
+ return { source: 'kubernetes', namespace: 'kradle-test', resources: { Repository: [repository] }, commands: [], events: [], permissions: [], storage: {} };
415
+ },
416
+ async list(kind) {
417
+ calls.push(['list', kind]);
418
+ return { items: [repository] };
419
+ },
420
+ async get(kind, name) {
421
+ calls.push(['get', kind, name]);
422
+ return repository;
423
+ },
424
+ async apply(resource) {
425
+ calls.push(['apply', resource.kind]);
426
+ return { operation: 'apply', resource };
427
+ },
428
+ async delete(kind, name) {
429
+ calls.push(['delete', kind, name]);
430
+ return { operation: 'delete', resource: null };
431
+ },
432
+ async createRepository(input) {
433
+ calls.push(['createRepository', input.name]);
434
+ return { operation: 'apply', command: 'kubectl apply -f -', resource: repository };
435
+ },
436
+ watch(resourcePath) {
437
+ calls.push(['watch', resourcePath]);
438
+ return { command: 'kubectl get repositories.kradle.a5c.ai --watch -o json' };
439
+ }
440
+ };
441
+
442
+ const controller = createKradleApiController({ resourceGateway });
443
+ const snapshot = await controller.snapshot();
444
+ const repositories = await controller.listRepositoriesForForge();
445
+ const view = await controller.getRepositoryForgeView('app');
446
+ const created = await controller.createRepository({ name: 'app', organizationRef: 'default' });
447
+
448
+ assert.equal(controller.role, 'kradle-api-controller');
449
+ assert.equal(snapshot.architecture.apiController.role, 'kradle-api-controller');
450
+ assert.ok(snapshot.architecture.apiController.mustNotOwn.includes('Kubernetes reconciliation loops'));
451
+ assert.equal(repositories[0].cloneUrl.endsWith('/app.git'), true);
452
+ assert.equal(view.primaryFlow, 'browse-code-open-pr-review-merge');
453
+ assert.equal(view.sections.some((section) => section.id === 'pull-requests'), true);
454
+ assert.equal(created.repository.actions.settings, '/orgs/default/repositories/app/settings');
455
+ assert.deepEqual(calls.map((call) => call[0]), ['snapshot', 'list', 'get', 'createRepository']);
456
+ });
457
+
458
+ test('Kubernetes resource gateway owns Kubernetes operations while reconciler owns status projection', async () => {
459
+ const repository = createResource('Repository', { name: 'app', namespace: 'kradle-test' }, { organizationRef: 'default', visibility: 'internal' });
460
+ const delegated = [];
461
+ const resourceClient = {
462
+ role: 'kubernetes-resource-client',
463
+ namespace: 'kradle-test',
464
+ resourceDefinitions: listResourceDefinitions(),
465
+ async snapshot() { delegated.push('snapshot'); return { source: 'kubernetes', namespace: 'kradle-test', resources: { Repository: [repository] } }; },
466
+ async listResource(kind) { delegated.push(`list:${kind}`); return { items: [repository] }; },
467
+ async getResource(kind, name) { delegated.push(`get:${kind}:${name}`); return repository; },
468
+ async applyResource(resource) { delegated.push(`apply:${resource.kind}`); return { operation: 'apply', resource }; },
469
+ async deleteResource(kind, name) { delegated.push(`delete:${kind}:${name}`); return { operation: 'delete' }; },
470
+ watchResource(resourcePath) { delegated.push(`watch:${resourcePath}`); return { command: 'kubectl get repositories.kradle.a5c.ai --watch -o json' }; }
471
+ };
472
+ const gateway = createKubernetesResourceGateway({ resourceClient });
473
+ const reconciler = createKradleKubernetesReconciler({ namespace: 'kradle-test' });
474
+ const applied = await gateway.createRepository({ name: 'app', organizationRef: 'default' });
475
+ const plan = reconciler.reconcileRepository(repository);
476
+
477
+ assert.equal(gateway.role, 'kubernetes-resource-gateway');
478
+ assert.ok(gateway.mustNotOwn.includes('Next.js page flow decisions'));
479
+ assert.equal(applied.resource.kind, 'Repository');
480
+ assert.equal(delegated.includes('apply:Repository'), true);
481
+ const client = createKubernetesResourceClient({ namespace: 'kradle-test' });
482
+ assert.equal(client.role, 'kubernetes-resource-client');
483
+ assert.equal(client.mustNotOwn.includes('forge DTO composition'), true);
484
+ assert.equal(reconciler.role, 'kradle-kubernetes-reconciler');
485
+ assert.ok(reconciler.mustNotOwn.includes('HTTP routes'));
486
+ assert.equal(plan.kind, 'RepositoryReconciliationPlan');
487
+ assert.equal(plan.syncIntents.some((intent) => intent.target === 'git-data-plane'), true);
488
+ assert.equal(reconciler.describeReconciliationScope().resources.some((resource) => resource.kind === 'Repository'), true);
489
+ });
490
+
491
+ test('identity access reconciler projects users permissions and SSH keys into Kradle status', () => {
492
+ const user = createResource('User', { name: 'alice', namespace: 'kradle-test' }, { organizationRef: 'default', email: 'alice@example.com', username: 'alice', teams: ['maintainers'], admin: false });
493
+ const disabledUser = createResource('User', { name: 'bob', namespace: 'kradle-test' }, { organizationRef: 'default', email: 'bob@example.com', username: 'bob', disabled: true });
494
+ const permission = createResource('RepositoryPermission', { name: 'app-alice', namespace: 'kradle-test' }, { organizationRef: 'default', repository: 'app', subject: 'alice', subjectKind: 'user', permission: 'write', revoked: true });
495
+ const sshKey = createResource('SSHKey', { name: 'alice-laptop', namespace: 'kradle-test' }, { organizationRef: 'default', owner: 'alice', title: 'laptop', scope: 'user', key: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKradleTest alice@example.com' });
496
+ const reconciler = createKradleKubernetesReconciler({ namespace: 'kradle-test' });
497
+ const activePlan = reconciler.reconcileIdentityAccess(user);
498
+ const disabledPlan = identityAccessReconciliationPlan(disabledUser, { namespace: 'kradle-test' });
499
+ const permissionPlan = reconciler.reconcileIdentityAccess(permission);
500
+ const sshPlan = reconciler.reconcileIdentityAccess(sshKey);
501
+ const aggregate = reconciler.reconcileIdentityAccessResources({ User: [user, disabledUser], RepositoryPermission: [permission], SSHKey: [sshKey] });
502
+
503
+ assert.equal(activePlan.desiredStatus.phase, 'Active');
504
+ assert.ok(activePlan.desiredStatus.groups.includes('team:maintainers'));
505
+ assert.ok(activePlan.syncIntents.some((intent) => intent.action === 'ensure-repository-user'));
506
+ assert.equal(disabledPlan.desiredStatus.phase, 'Disabled');
507
+ assert.ok(disabledPlan.syncIntents.some((intent) => intent.action === 'suspend-user'));
508
+ assert.equal(permissionPlan.desiredStatus.phase, 'Revoked');
509
+ assert.equal(permissionPlan.syncIntents[0].action, 'revoke-repository-permission');
510
+ assert.match(sshPlan.desiredStatus.fingerprint, /^sha256:[A-Za-z0-9_-]+$/);
511
+ assert.equal(sshPlan.syncIntents[0].action, 'sync-ssh-key');
512
+ assert.deepEqual(aggregate.counts, { User: 2, RepositoryPermission: 1, SSHKey: 1 });
513
+ assert.equal(aggregate.desiredStatuses.filter((status) => status.phase === 'Revoked').length, 1);
514
+ assert.ok(aggregate.syncIntents.some((intent) => intent.action === 'revoke-repository-permission'));
515
+ });
516
+
517
+ test('controller boundary source keeps kubectl out of API facade and UI flow out of reconciler', () => {
518
+ const apiSource = readFileSync('src/api-controller.js', 'utf8');
519
+ const gatewaySource = readFileSync('src/kubernetes-resource-gateway.js', 'utf8');
520
+ const kubernetesSource = readFileSync('src/kubernetes-controller.js', 'utf8');
521
+
522
+ assert.equal(apiSource.includes('node:child_process'), false);
523
+ assert.equal(apiSource.includes('spawnSync'), false);
524
+ assert.equal(apiSource.includes('KRADLE_API_CONTROLLER_BOUNDARY'), true);
525
+ assert.equal(gatewaySource.includes('KUBERNETES_RESOURCE_GATEWAY_BOUNDARY'), true);
526
+ assert.equal(kubernetesSource.includes('KRADLE_KUBERNETES_RECONCILER_BOUNDARY'), true);
527
+ assert.equal(kubernetesSource.includes('Next.js page flows'), true);
528
+ assert.equal(kubernetesSource.includes('/repositories/${repo}'), false);
529
+ });test('RBAC and admission enforce Kubernetes-style policy while supporting audit mode', () => {
530
+ const controlPlane = new ControlPlane();
531
+ controlPlane.addAdmissionPolicy(createAdmissionPolicy({ name: 'descriptive-pr-title', mode: 'enforce', match: ({ resource }) => resource.kind === 'PullRequest', validate: ({ resource }) => resource.spec.title?.length >= 8, message: 'title too short' }));
532
+ assert.throws(() => controlPlane.create(createResource('PullRequest', { name: 'bad' }, { organizationRef: 'default', repository: 'app', title: 'tiny' }), developer), /title too short/);
533
+ const accepted = controlPlane.create(createResource('PullRequest', { name: 'good' }, { organizationRef: 'default', repository: 'app', title: 'Implement policy preview flow' }), developer);
534
+ assert.equal(accepted.status.storage, 'postgres');
535
+ assert.throws(() => controlPlane.create(createResource('Repository', { name: 'denied' }, { organizationRef: 'default', visibility: 'private' }), developer), /RBAC denied/);
536
+ const policyModel = createPolicyRolloutModel({ name: 'descriptive-pr-title', mode: 'audit' });
537
+ assert.deepEqual(policyModel.rollout, ['preview', 'audit', 'enforce']);
538
+ });
539
+
540
+ test('watch events and audit-mode admission warnings remain inspectable', () => {
541
+ const controlPlane = new ControlPlane();
542
+ const events = [];
543
+ const stop = controlPlane.watch('PullRequest', (event) => events.push(event));
544
+ controlPlane.addAdmissionPolicy(createAdmissionPolicy({ name: 'audit-title', mode: 'audit', match: ({ resource }) => resource.kind === 'PullRequest', validate: ({ resource }) => resource.spec.title?.includes('audit'), message: 'title should mention audit' }));
545
+ const pr = controlPlane.create(createResource('PullRequest', { name: 'audit-pr' }, { organizationRef: 'default', repository: 'app', title: 'Implement warning visibility' }), developer);
546
+ stop();
547
+ assert.equal(pr.status.storage, 'postgres');
548
+ assert.equal(events.length, 1);
549
+ assert.equal(controlPlane.auditLog.at(-1).warnings[0].policy, 'audit-title');
550
+ });
551
+
552
+ test('Gitea data plane keeps receive-pack warm and protected', () => {
553
+ const controlPlane = new ControlPlane();
554
+ const git = new GiteaGitService({ controlPlane, stores: [new GiteaRepositoryStore({ name: 'gitea-primary', receivePackReady: true })] });
555
+ const repository = git.createRepository({ name: 'app' }, repoAdmin);
556
+ controlPlane.create(createResource('BranchProtection', { name: 'main', namespace: 'kradle-org-default' }, { organizationRef: 'default', refs: ['refs/heads/main'], requirePullRequest: true }), repoAdmin);
557
+ const route = git.route('app');
558
+ assert.equal(route.backend, 'gitea');
559
+ assert.equal(route.receivePackReady, true);
560
+ assert.equal(repository.spec.gitHosting.backend, 'gitea');
561
+ assert.equal(repository.spec.gitHosting.integrationPlan.backend, 'gitea');
562
+ assert.ok(repository.spec.gitHosting.integrationPlan.operations.some((step) => step.action === 'addDeployKey'));
563
+ assert.throws(() => git.receivePack({ repository: 'app', ref: 'refs/heads/main', newRev: '1'.repeat(40), actor: developer }), /requires pull request/);
564
+ const event = git.receivePack({ repository: 'app', ref: 'refs/heads/main', newRev: '2'.repeat(40), actor: repoAdmin });
565
+ assert.equal(event.backend, 'gitea');
566
+ assert.equal(event.store, 'gitea-primary');
567
+ assert.match(event.remoteUrl, /gitea/);
568
+ controlPlane.create(createResource('RefPolicy', { name: 'deny-internal', namespace: 'kradle-org-default' }, { organizationRef: 'default', deny: ['refs/internal/'] }), repoAdmin);
569
+ assert.throws(() => git.receivePack({ repository: 'app', ref: 'refs/internal/secret', newRev: '3'.repeat(40), actor: repoAdmin }), /RefPolicy deny-internal denied/);
570
+ assert.equal(git.recordObject({ repository: 'app', key: 'lfs/abc', size: 128 }).storage, 'object-storage');
571
+ assert.equal(git.enqueueSearchIndex({ repository: 'app', commit: '2'.repeat(40), paths: ['README.md'] }).status, 'queued');
572
+ });
573
+
574
+
575
+ test('runner scheduler isolates fork PR jobs and supports resume reruns', () => {
576
+ const controlPlane = new ControlPlane();
577
+ const runners = new RunnerScheduler({ controlPlane });
578
+ const pool = runners.createRunnerPool({ name: 'trusted-linux', warmReplicas: 1, maxReplicas: 3 }, platform);
579
+ assert.equal(runners.planReplicas(pool, 9), 3);
580
+ const run = runners.startPipeline({ name: 'pr-2', repository: 'app', ref: 'refs/pull/2/head', actor: developer, fork: true, steps: ['checkout', 'test', 'publish'] }, developer);
581
+ assert.equal(run.jobs[0].spec.serviceAccount.scopes.secrets, false);
582
+ assert.equal(run.jobs[0].spec.serviceAccount.scopes.clusterApi, false);
583
+ const rerun = runners.rerunFromStep(run.pipeline, 'test', developer);
584
+ assert.equal(rerun.pipeline.spec.resumeFrom, 'test');
585
+ });
586
+
587
+ test('webhooks are signed, stored, inspectable, and replayable', () => {
588
+ const controlPlane = new ControlPlane();
589
+ const bus = new WebhookBus({ controlPlane, secret: 'test-secret' });
590
+ bus.subscribe({ name: 'chatops', url: 'https://hooks.example.test', events: ['pullrequest.created'] }, repoAdmin);
591
+ const first = bus.deliver({ subscriptionName: 'chatops', eventType: 'pullrequest.created', payload: { pr: 'pr-1' } }, repoAdmin);
592
+ const failed = bus.deliver({ subscriptionName: 'chatops', eventType: 'pullrequest.created', payload: { pr: 'pr-2' }, response: { status: 503, body: 'downstream unavailable' } }, repoAdmin);
593
+ const replay = bus.replay(first, repoAdmin);
594
+ assert.equal(first.kind, 'WebhookDelivery');
595
+ assert.equal(first.status.phase, 'Delivered');
596
+ assert.equal(bus.inspect(failed).phase, 'Failed');
597
+ assert.equal(bus.inspect(failed).replayable, true);
598
+ assert.notEqual(first.spec.signature, replay.spec.signature);
599
+ assert.equal(controlPlane.list('WebhookDelivery').items.length, 3);
600
+ });
601
+
602
+
603
+ test('component catalog and lifecycle snapshot cover every implementation area', () => {
604
+ const demo = createKradleMvpDemo();
605
+ demo.smoke = runSmokeAssertions(demo);
606
+ const catalog = createKradleComponentCatalog(demo);
607
+ const snapshot = createKradleLifecycleSnapshot(demo, { packageInfo: { version: 'test' }, generatedAt: 'test-time' });
608
+ assert.equal(catalog.length, 7);
609
+ assert.equal(snapshot.status, 'ready-for-local-development');
610
+ assert.ok(snapshot.components.some((component) => component.id === 'control-plane' && component.implemented));
611
+ assert.ok(snapshot.components.some((component) => component.id === 'runners-ci' && component.resources.includes('Pipeline')));
612
+ assert.ok(snapshot.operations.releaseGates.includes('e2e package lifecycle tests'));
613
+ assert.ok(snapshot.validation.every((assertion) => assertion.passed));
614
+ });
615
+ test('MVP smoke path covers resources, UI, operations, and release gates', () => {
616
+ const demo = createKradleMvpDemo();
617
+ const smoke = runSmokeAssertions(demo);
618
+ assert.equal(smoke.ok, true);
619
+ assert.ok(demo.ui.dashboard.excellentFlows.includes('Open and review a PR'));
620
+ assert.ok(demo.operations.backupPlan.restoreOrder.includes('Postgres'));
621
+ assert.ok(demo.operations.backupPlan.restoreOrder.includes('repository data'));
622
+ assert.ok(demo.operations.installManifests.includes('kradle-gitea'));
623
+ assert.ok(demo.operations.installManifests.includes('kind: Application'));
624
+ assert.equal(demo.resources.repository.spec.gitHosting.backend, 'gitea');
625
+ assert.ok(demo.operations.releaseGates.includes('docs and ontology coverage'));
626
+ assert.ok(demo.operations.observability.alerts.includes('runner saturation'));
627
+ });
628
+
629
+ import { execFileSync } from 'node:child_process';
630
+ import { readFileSync } from 'node:fs';
631
+
632
+ test('handoff summary and package metadata expose runnable lifecycle surfaces', () => {
633
+ const packageInfo = JSON.parse(readFileSync('package.json', 'utf8'));
634
+ const demo = createKradleMvpDemo();
635
+ demo.smoke = runSmokeAssertions(demo);
636
+ const summary = createKradleHandoffSummary(demo, { packageInfo, generatedAt: 'test-time' });
637
+ assert.equal(packageInfo.main, './src/index.js');
638
+ assert.equal(packageInfo.bin['kradle-demo'], './bin/kradle-demo.mjs');
639
+ assert.equal(packageInfo.scripts.demo, 'node bin/kradle-demo.mjs');
640
+ assert.equal(summary.entrypoints.cli, './bin/kradle-demo.mjs');
641
+ assert.equal(summary.commands.check, 'npm run check');
642
+ assert.ok(summary.docs.includes('docs/architecture-spec.md'));
643
+ assert.ok(summary.releaseGates.includes('docs and ontology coverage'));
644
+ assert.equal(summary.smoke.ok, true);
645
+ assert.equal(summary.generatedAt, 'test-time');
646
+ });
647
+
648
+ test('CLI demo emits machine-readable MVP handoff summary', () => {
649
+ const output = execFileSync(process.execPath, ['bin/kradle-demo.mjs', '--json'], { encoding: 'utf8' });
650
+ const summary = JSON.parse(output);
651
+ assert.equal(summary.project, 'Kradle');
652
+ assert.equal(summary.entrypoints.library, './src/index.js');
653
+ assert.equal(summary.commands.demo, 'npm run demo');
654
+ assert.equal(summary.smoke.ok, true);
655
+ assert.ok(summary.excellentFlows.includes('Open and review a PR'));
656
+ });
657
+
658
+
659
+
660
+ import { KradleRuntime, createKradleHttpServer, createKradleRuntime } from '../src/index.js';
661
+
662
+ test('runtime executes PR checks review and merge lifecycle with persisted resources', () => {
663
+ const runtime = createKradleRuntime();
664
+ const created = runtime.createPullRequest({ repository: 'kradle-demo', title: 'Ship runnable lifecycle implementation' });
665
+ assert.equal(created.pullRequest.status.phase, 'Open');
666
+ assert.throws(() => runtime.mergePullRequest({ pullRequest: created.pullRequest.metadata.name }), /not mergeable/);
667
+ const checks = runtime.completePipeline({ pipeline: created.pipeline.metadata.name });
668
+ assert.equal(checks.pipeline.status.phase, 'Succeeded');
669
+ const review = runtime.addReview({ pullRequest: created.pullRequest.metadata.name, decision: 'approved' });
670
+ assert.equal(review.status.phase, 'Approved');
671
+ const merged = runtime.mergePullRequest({ pullRequest: created.pullRequest.metadata.name });
672
+ assert.equal(merged.pullRequest.status.phase, 'Merged');
673
+ assert.equal(merged.delivery.kind, 'WebhookDelivery');
674
+ const snapshot = runtime.snapshot();
675
+ assert.equal(snapshot.resources.PullRequest[0].status.phase, 'Merged');
676
+ assert.ok(snapshot.events.some((event) => event.type === 'git.receive-pack' && event.resource.backend === 'gitea'));
677
+ assert.equal(snapshot.resources.Repository[0].spec.gitHosting.backend, 'gitea');
678
+ });
679
+
680
+ test('runtime snapshot export restores durable state and continues lifecycle', () => {
681
+ const runtime = createKradleRuntime();
682
+ const created = runtime.createPullRequest({ repository: 'kradle-demo', title: 'Persist runtime lifecycle across restart' });
683
+ runtime.completePipeline({ pipeline: created.pipeline.metadata.name });
684
+ runtime.addReview({ pullRequest: created.pullRequest.metadata.name, decision: 'approved' });
685
+ runtime.mergePullRequest({ pullRequest: created.pullRequest.metadata.name });
686
+ const exported = runtime.exportSnapshot();
687
+ const restored = KradleRuntime.fromSnapshot(exported);
688
+ const restoredSnapshot = restored.snapshot();
689
+ assert.equal(restoredSnapshot.resources.PullRequest[0].status.phase, 'Merged');
690
+ assert.equal(restoredSnapshot.resources.WebhookDelivery.length, runtime.snapshot().resources.WebhookDelivery.length);
691
+ const next = restored.createPullRequest({ repository: 'kradle-demo', title: 'Continue after snapshot import' });
692
+ assert.equal(next.pullRequest.metadata.name, 'pr-2');
693
+ assert.equal(restored.exportSnapshot().controlPlane.stores.postgres.some((resource) => resource.kind === 'PullRequest' && resource.metadata.name === 'pr-2'), true);
694
+ });
695
+
696
+ test('runtime snapshot preserves git object storage and search index data', () => {
697
+ const runtime = createKradleRuntime();
698
+ runtime.git.recordObject({ repository: 'kradle-demo', key: 'lfs/blob.bin', size: 512, mediaType: 'application/octet-stream' });
699
+ runtime.git.enqueueSearchIndex({ repository: 'kradle-demo', commit: 'a'.repeat(40), paths: ['src/index.js', 'README.md'] });
700
+
701
+ const exported = runtime.exportSnapshot();
702
+ assert.equal(exported.git.backend.type, 'gitea');
703
+ assert.equal(exported.git.integrationPlans['kradle-demo'].backend, 'gitea');
704
+ assert.equal(exported.git.stores[0].objects['kradle-demo'][0].key, 'lfs/blob.bin');
705
+ assert.deepEqual(exported.git.stores[0].searchIndex['kradle-demo'][0].paths, ['src/index.js', 'README.md']);
706
+
707
+ const restored = KradleRuntime.fromSnapshot(exported);
708
+ const restoredGit = restored.exportSnapshot().git;
709
+ assert.equal(restoredGit.stores[0].objects['kradle-demo'][0].size, 512);
710
+ assert.equal(restoredGit.stores[0].searchIndex['kradle-demo'][0].commit, 'a'.repeat(40));
711
+ const restoredRoute = restored.git.uploadPack({ repository: 'kradle-demo' });
712
+ assert.equal(restoredRoute.cacheable, true);
713
+ assert.equal(restoredRoute.backend, 'gitea');
714
+ });
715
+
716
+ test('HTTP API exposes executable runtime endpoints', async () => {
717
+ const runtime = createKradleRuntime();
718
+ const server = createKradleHttpServer({ runtime });
719
+ await new Promise((resolve) => server.listen(0, resolve));
720
+ const base = `http://127.0.0.1:${server.address().port}`;
721
+ try {
722
+ const created = await fetchJson(`${base}/api/orgs/default/pullrequests`, { method: 'POST', body: { repository: 'kradle-demo', title: 'Exercise HTTP lifecycle runtime' } });
723
+ assert.equal(created.pullRequest.kind, 'PullRequest');
724
+ await fetchJson(`${base}/api/orgs/default/pullrequests/${created.pullRequest.metadata.name}/checks/complete`, { method: 'POST', body: {} });
725
+ await fetchJson(`${base}/api/orgs/default/pullrequests/${created.pullRequest.metadata.name}/reviews`, { method: 'POST', body: { decision: 'approved' } });
726
+ const merged = await fetchJson(`${base}/api/orgs/default/pullrequests/${created.pullRequest.metadata.name}/merge`, { method: 'POST', body: {} });
727
+ assert.equal(merged.pullRequest.status.phase, 'Merged');
728
+ const snapshot = await fetchJson(`${base}/api/orgs/default/snapshot`);
729
+ assert.ok(snapshot.resources.WebhookDelivery.length >= 2);
730
+ const restored = await fetchJson(`${base}/api/orgs/default/snapshot`, { method: 'POST', body: snapshot.export });
731
+ assert.equal(restored.resources.PullRequest[0].status.phase, 'Merged');
732
+ assert.equal(restored.export.controlPlane.stores.postgres.some((resource) => resource.kind === 'PullRequest'), true);
733
+ } finally {
734
+ await new Promise((resolve) => server.close(resolve));
735
+ }
736
+ });
737
+
738
+
739
+ test('HTTP API exposes Git object storage and search indexing runtime endpoints', async () => {
740
+ const runtime = createKradleRuntime();
741
+ const server = createKradleHttpServer({ runtime });
742
+ await new Promise((resolve) => server.listen(0, resolve));
743
+ const base = `http://127.0.0.1:${server.address().port}`;
744
+ try {
745
+ const object = await fetchJson(`${base}/api/orgs/default/repositories/kradle-demo/objects`, {
746
+ method: 'POST',
747
+ body: { key: 'lfs/http.bin', size: 1024, mediaType: 'application/octet-stream' }
748
+ });
749
+ assert.equal(object.repository, 'kradle-demo');
750
+ assert.equal(object.storage, 'object-storage');
751
+
752
+ const index = await fetchJson(`${base}/api/orgs/default/repositories/kradle-demo/search-index`, {
753
+ method: 'POST',
754
+ body: { commit: 'b'.repeat(40), paths: ['src/http-server.js', 'README.md'] }
755
+ });
756
+ assert.equal(index.status, 'queued');
757
+ assert.deepEqual(index.paths, ['src/http-server.js', 'README.md']);
758
+
759
+ const snapshot = await fetchJson(`${base}/api/orgs/default/snapshot`);
760
+ assert.equal(snapshot.export.git.stores[0].objects['kradle-demo'][0].key, 'lfs/http.bin');
761
+ assert.equal(snapshot.export.git.stores[0].searchIndex['kradle-demo'][0].commit, 'b'.repeat(40));
762
+ } finally {
763
+ await new Promise((resolve) => server.close(resolve));
764
+ }
765
+ });
766
+
767
+ async function fetchJson(url, { method = 'GET', body } = {}) {
768
+ const response = await fetch(url, { method, headers: body ? { 'content-type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined });
769
+ const value = await response.json();
770
+ if (!response.ok) throw new Error(`${response.status} ${JSON.stringify(value)}`);
771
+ return value;
772
+ }
773
+
774
+
775
+
776
+
777
+
778
+
779
+