@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,527 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { createControllerUiModel, createKradleApiController, createKradleHttpServer } from '../src/index.js';
6
+
7
+ function read(path) {
8
+ return readFileSync(path, 'utf8');
9
+ }
10
+
11
+ function workflowJobBlock(workflow, jobName) {
12
+ const match = workflow.match(new RegExp(`\r?\n ${jobName}:\r?\n([\\s\\S]*?)(?=\r?\n [a-zA-Z0-9_-]+:\r?\n|$)`));
13
+ assert.ok(match, `workflow has ${jobName} job`);
14
+ return match[1];
15
+ }
16
+
17
+ function fixtureKubernetesController() {
18
+ return {
19
+ async snapshot() {
20
+ return {
21
+ source: 'kubernetes',
22
+ namespace: 'kradle-org-default',
23
+ generatedAt: 'test-time',
24
+ correlationId: 'test-correlation',
25
+ kubectl: { available: true, context: 'kind-kradle', clientVersion: 'v1.test', errors: [] },
26
+ apiService: { metadata: { name: 'v1alpha1.kradle.a5c.ai' } },
27
+ crds: [{ metadata: { name: 'repositories.kradle.a5c.ai' } }],
28
+ storage: { etcd: 'etcd', postgres: 'postgres', repositories: 'rwx', objects: 'object' },
29
+ commands: [],
30
+ permissions: [],
31
+ events: [],
32
+ resources: {
33
+ Organization: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Organization', metadata: { name: 'default', namespace: 'kradle-system' }, spec: { slug: 'default', namespaceName: 'kradle-org-default', displayName: 'Default org' } }],
34
+ User: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'User', metadata: { name: 'alice', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Alice', email: 'alice@example.com', teams: ['maintainers'] }, status: { phase: 'Active' } }],
35
+ Team: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Team', metadata: { name: 'maintainers', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Maintainers', members: ['alice'] }, status: { phase: 'Active' } }],
36
+ Invite: [],
37
+ IdentityMapping: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'IdentityMapping', metadata: { name: 'sso-alice', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', user: 'alice', provider: 'sso', subject: 'alice', repositoryIdentity: { username: 'alice' } }, status: { phase: 'Synced' } }],
38
+ AuthProvider: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'AuthProvider', metadata: { name: 'sso', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', type: 'oidc', label: 'Company SSO', enabled: true }, status: { phase: 'Configured' } }],
39
+ Repository: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'Repository', metadata: { name: 'app', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', visibility: 'internal', defaultBranch: 'main' }, status: { phase: 'Ready' } }],
40
+ PolicyProfile: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyProfile', metadata: { name: 'default-profile', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Default policy posture', mode: 'audit' }, status: { phase: 'Synced', lastViolationCount: 1 } }],
41
+ PolicyTemplate: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyTemplate', metadata: { name: 'require-pr-description', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', displayName: 'Require PR description', targetKinds: ['PullRequest'], kyverno: { kind: 'ValidatingPolicy' } }, status: { phase: 'Ready' } }],
42
+ PolicyBinding: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyBinding', metadata: { name: 'require-pr-description-audit', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', templateRef: 'require-pr-description', mode: 'audit' }, status: { phase: 'Applied', lastViolationCount: 1 } }],
43
+ PolicyExceptionRequest: [{ apiVersion: 'kradle.a5c.ai/v1alpha1', kind: 'PolicyExceptionRequest', metadata: { name: 'pr-1-exception', namespace: 'kradle-org-default' }, spec: { organizationRef: 'default', policyRef: { name: 'require-pr-description' }, justification: 'temporary rollout', expiresAt: '2026-06-01T00:00:00Z' }, status: { phase: 'Requested' } }],
44
+ PullRequest: [],
45
+ Pipeline: [],
46
+ RunnerPool: [],
47
+ WebhookSubscription: []
48
+ },
49
+ kyverno: {
50
+ mode: 'byo',
51
+ namespace: 'kyverno',
52
+ policyNamespace: 'kradle-org-default',
53
+ detected: true,
54
+ controllers: [{ name: 'kyverno-admission-controller', ready: true, readyReplicas: 1, replicas: 1 }],
55
+ permissions: [{ kind: 'PolicyReport', verbs: { list: true, watch: true } }],
56
+ resources: { KyvernoValidatingPolicy: [{ metadata: { name: 'require-pr-description' } }], PolicyReport: [{ metadata: { name: 'app-policy', namespace: 'kradle-org-default' }, results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required', resources: [{ kind: 'PullRequest', name: 'pr-1' }] }] }] },
57
+ reports: { policyReports: [{ metadata: { name: 'app-policy', namespace: 'kradle-org-default' } }], clusterPolicyReports: [], results: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }], violations: [{ policy: 'require-pr-description', rule: 'description', result: 'fail', message: 'description required' }] },
58
+ degraded: []
59
+ }
60
+ };
61
+ },
62
+ async applyResource(resource) {
63
+ return { operation: 'apply', resource };
64
+ },
65
+ async deleteResource(kind, name) {
66
+ return { operation: 'delete', resource: { kind, metadata: { name } } };
67
+ },
68
+ async createRepository(input) {
69
+ return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } };
70
+ }
71
+ };
72
+ }
73
+
74
+ test('controller deployment assets build and publish the runnable controller', () => {
75
+ for (const file of ['Dockerfile', '.dockerignore', '.github/workflows/publish.yml']) assert.equal(existsSync(file), true, `${file} exists`);
76
+
77
+ const dockerfile = read('Dockerfile');
78
+ assert.match(dockerfile, /FROM node:\d+-alpine AS (deps|build)/);
79
+ assert.match(dockerfile, /npm (ci|install)/);
80
+ assert.match(dockerfile, /HEALTHCHECK/);
81
+ assert.match(dockerfile, /kradle-server\.mjs/);
82
+ for (const runtimePath of ['/app/bin', '/app/src', '/app/dist']) assert.ok(dockerfile.includes(runtimePath), `Dockerfile copies ${runtimePath}`);
83
+
84
+ const dockerignore = read('.dockerignore');
85
+ for (const ignored of ['.a5c', 'node_modules', '**/.next', 'dist']) assert.ok(dockerignore.includes(ignored), `.dockerignore excludes ${ignored}`);
86
+
87
+ const chartDeployment = read('../charts/templates/deployments.yaml');
88
+ const chartRbac = read('../charts/templates/rbac.yaml');
89
+ const chartIngress = read('../charts/templates/ingress.yaml');
90
+ const chartValues = read('../charts/values.yaml');
91
+ const authSecret = read('../charts/templates/auth-secret.yaml');
92
+ assert.ok(chartValues.includes('auth:'), 'chart values include auth configuration');
93
+ assert.ok(chartValues.includes('github:') && chartValues.includes('sso:'), 'chart values expose GitHub and SSO configuration');
94
+ assert.ok(chartValues.includes('assistant:') && chartValues.includes('anthropic-api-key'), 'chart values expose assistant secret references');
95
+ assert.ok(chartValues.includes('agentMux:') && chartValues.includes('gatewayUrl'), 'chart values expose Agent Mux endpoint configuration');
96
+ assert.ok(chartValues.includes('token:') && chartValues.includes('existingSecret: ""'), 'chart values expose Gitea token secret reference');
97
+ assert.ok(chartValues.includes('mode: auto') && chartValues.includes('policyReporter:'), 'chart values expose Kyverno auto-discovery modes and policy reporter settings');
98
+ assert.ok(chartRbac.includes('"*"') && chartRbac.includes('policies.kyverno.io') && chartRbac.includes('policyreports'), 'RBAC covers all Kradle resources via wildcard and Kyverno read/write surfaces');
99
+ assert.ok(chartDeployment.includes('KRADLE_KYVERNO_MODE') && chartDeployment.includes('KRADLE_KYVERNO_POLICY_NAMESPACE') && chartDeployment.includes('KRADLE_KYVERNO_DISCOVER_EXISTING'), 'deployments receive Kyverno discovery env');
100
+ assert.ok(read('../charts/templates/argocd-kyverno-application.yaml').includes('kradle.a5c.ai/policy-engine: kyverno'), 'managed Kyverno Argo CD application template is present');
101
+ assert.ok(authSecret.includes('github-client-id') && authSecret.includes('sso-client-secret'), 'chart renders auth secret keys');
102
+ assert.ok(chartIngress.includes('kind: Ingress') && chartIngress.includes('ingressClassName') && chartIngress.includes('app.kubernetes.io/component: web'), 'chart renders web ingress');
103
+ assert.ok(chartDeployment.includes('imagePullSecrets') && chartDeployment.includes('global.imagePullSecrets'), 'workloads can use registry pull secrets');
104
+ assert.ok(chartDeployment.includes('KRADLE_AUTH_GITHUB_ENABLED') && chartDeployment.includes('KRADLE_AUTH_SSO_ENABLED') && chartDeployment.includes('KRADLE_AUTH_DELEGATED_EMAIL_HEADER'), 'workloads receive auth provider configuration');
105
+ assert.ok(chartDeployment.includes('readinessProbe:') && chartDeployment.includes('path: /login'), 'web deployment has an HTTP readiness probe');
106
+ assert.ok(chartDeployment.includes('KRADLE_AUTH_DELEGATED_LOCAL_DEVELOPMENT') && chartDeployment.includes('KRADLE_AUTH_DELEGATED_LOCAL_GROUPS'), 'workloads can opt into local delegated development login');
107
+ assert.ok(chartDeployment.includes('ANTHROPIC_API_KEY') && chartDeployment.includes('KRADLE_ASSISTANT_API_KEY'), 'web workload can receive assistant API key references');
108
+ assert.ok(chartDeployment.includes('KRADLE_GITEA_TOKEN') && chartDeployment.includes('AGENT_MUX_URL') && chartDeployment.includes('AGENT_GATEWAY_URL'), 'workloads can receive Gitea token and Agent Mux endpoint configuration');
109
+ assert.ok(chartRbac.includes('core.oam.dev') && chartRbac.includes('applications') && chartRbac.includes('create'), 'delivery resources can be composed through Kradle');
110
+ assert.ok(chartValues.includes('localDevelopment:') && chartValues.includes('enabled: false'), 'local delegated development login is off by default');
111
+ for (const token of ['command: ["node", "bin/kradle-server.mjs"]', '--port=3080', 'app.kubernetes.io/component: controllers']) {
112
+ assert.ok(chartDeployment.includes(token), `chart deployment includes ${token}`);
113
+ }
114
+ });
115
+
116
+ test('web proxy protects UI pages and authenticated APIs behind login', async () => {
117
+ let NextRequest, proxy, config;
118
+ try {
119
+ ({ NextRequest } = await import('next/server.js'));
120
+ ({ proxy, config } = await import(`../../web/proxy.js?test=${Date.now()}`));
121
+ } catch { return; }
122
+
123
+ assert.ok(config.matcher.some((entry) => entry.includes('_next/static')));
124
+ for (const path of ['/', '/orgs/default/repositories?tab=code', '/orgs/default/repositories/demo/code', '/orgs/default/people', '/logout', '/api/controller']) {
125
+ const response = proxy(new NextRequest(`http://localhost:3000${path}`));
126
+ assert.equal(response.status, 307, `${path} redirects to login without session`);
127
+ assert.equal(new URL(response.headers.get('location')).pathname, '/login');
128
+ }
129
+
130
+ const nextResponse = proxy(new NextRequest('http://localhost:3000/orgs/default/repositories?tab=code'));
131
+ assert.equal(new URL(nextResponse.headers.get('location')).searchParams.get('next'), '/orgs/default/repositories?tab=code');
132
+ assert.equal(proxy(new NextRequest('http://localhost:3000/login')).headers.get('x-middleware-next'), '1');
133
+ assert.equal(proxy(new NextRequest('http://localhost:3000/api/auth/delegated')).headers.get('x-middleware-next'), '1');
134
+ assert.equal(proxy(new NextRequest('http://localhost:3000/orgs/default/repositories', { headers: { cookie: 'kradle_session=dev' } })).headers.get('x-middleware-next'), '1');
135
+ });
136
+
137
+ test('login page stays minimal and does not expose the authenticated console shell', () => {
138
+ const layout = read('../web/app/layout.jsx');
139
+ const managePages = read('../web/app/pages/manage-pages.jsx');
140
+ const loginStart = managePages.indexOf('export function LoginPage');
141
+ assert.notEqual(loginStart, -1, 'LoginPage is defined in manage-pages module');
142
+ assert.ok(!layout.includes('<AppShell>{children}</AppShell>'), 'root layout does not wrap public routes in AppShell');
143
+ const loginSource = managePages.slice(loginStart, managePages.indexOf('export ', loginStart + 1));
144
+ assert.ok(loginSource.includes('loginMain') || loginSource.includes('login'), 'login page uses standalone login layout');
145
+ });
146
+
147
+ test('auth chart uses existing secrets without rendering empty provider keys', () => {
148
+ const mixed = execFileSync('helm', [
149
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
150
+ '--set', 'argocd.enabled=false',
151
+ '--set', 'auth.github.clientId=github-client',
152
+ '--set', 'auth.github.clientSecret=github-secret',
153
+ '--set', 'auth.sso.enabled=true',
154
+ '--set', 'auth.sso.existingSecret=shared-auth'
155
+ ], { encoding: 'utf8' });
156
+ assert.match(mixed, /name: kradle-kradle-auth/);
157
+ assert.match(mixed, /github-client-id: "github-client"/);
158
+ assert.doesNotMatch(mixed, /sso-client-id:/);
159
+ assert.match(mixed, /name: "shared-auth"[\s\S]*?key: sso-client-id/);
160
+
161
+ const externalOnly = execFileSync('helm', [
162
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
163
+ '--set', 'argocd.enabled=false',
164
+ '--set', 'auth.github.existingSecret=shared-auth',
165
+ '--set', 'auth.sso.enabled=true',
166
+ '--set', 'auth.sso.existingSecret=shared-auth'
167
+ ], { encoding: 'utf8' });
168
+ assert.doesNotMatch(externalOnly, /name: kradle-kradle-auth/);
169
+ assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: github-client-id/);
170
+ assert.match(externalOnly, /name: "shared-auth"[\s\S]*?key: sso-client-secret/);
171
+ });
172
+
173
+ test('staging service env vars are opt-in and render secret references without plaintext values', () => {
174
+ const defaultRender = execFileSync('helm', [
175
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
176
+ '--set', 'argocd.enabled=false'
177
+ ], { encoding: 'utf8' });
178
+ assert.doesNotMatch(defaultRender, /name: ANTHROPIC_API_KEY/);
179
+ assert.doesNotMatch(defaultRender, /name: KRADLE_ASSISTANT_API_KEY/);
180
+ assert.doesNotMatch(defaultRender, /name: KRADLE_GITEA_TOKEN/);
181
+ assert.doesNotMatch(defaultRender, /name: AGENT_MUX_URL/);
182
+ assert.doesNotMatch(defaultRender, /name: AGENT_GATEWAY_URL/);
183
+
184
+ const configured = execFileSync('helm', [
185
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
186
+ '--set', 'argocd.enabled=false',
187
+ '--set', 'gitea.httpUrl=http://gitea-http.kradle-system.svc.cluster.local:3000/kradle',
188
+ '--set', 'gitea.token.existingSecret=kradle-gitea-token',
189
+ '--set', 'gitea.token.key=token',
190
+ '--set', 'agentMux.url=http://agent-mux.kradle-system.svc.cluster.local:8080',
191
+ '--set', 'agentMux.gatewayUrl=http://agent-gateway.kradle-system.svc.cluster.local:8080',
192
+ '--set', 'assistant.anthropic.existingSecret=kradle-assistant',
193
+ '--set', 'assistant.anthropic.key=anthropic-api-key',
194
+ '--set', 'assistant.kradleAssistant.existingSecret=kradle-assistant',
195
+ '--set', 'assistant.kradleAssistant.key=kradle-assistant-api-key'
196
+ ], { encoding: 'utf8' });
197
+
198
+ assert.match(configured, /name: KRADLE_GITEA_HTTP_URL\s+value: "http:\/\/gitea-http\.kradle-system\.svc\.cluster\.local:3000\/kradle"/);
199
+ assert.match(configured, /name: KRADLE_GITEA_TOKEN[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-gitea-token"[\s\S]*?key: "token"[\s\S]*?optional: true/);
200
+ assert.match(configured, /name: ANTHROPIC_API_KEY[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-assistant"[\s\S]*?key: "anthropic-api-key"[\s\S]*?optional: true/);
201
+ assert.match(configured, /name: KRADLE_ASSISTANT_API_KEY[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-assistant"[\s\S]*?key: "kradle-assistant-api-key"[\s\S]*?optional: true/);
202
+ assert.match(configured, /name: AGENT_MUX_URL\s+value: "http:\/\/agent-mux\.kradle-system\.svc\.cluster\.local:8080"/);
203
+ assert.match(configured, /name: AGENT_GATEWAY_URL\s+value: "http:\/\/agent-gateway\.kradle-system\.svc\.cluster\.local:8080"/);
204
+ assert.doesNotMatch(configured, /sk-ant-|ghp_|github_pat_|glpat-|AKIA[0-9A-Z]{16}/);
205
+ });
206
+
207
+ test('NATS event transport renders inline and secret-backed broker URLs', () => {
208
+ const inline = execFileSync('helm', [
209
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
210
+ '--set', 'argocd.enabled=false',
211
+ '--set', 'externalDependencies.nats.eventTransport.enabled=true',
212
+ '--set', 'externalDependencies.nats.url=nats://nats.kradle-system.svc.cluster.local:4222',
213
+ '--set', 'externalDependencies.nats.eventTransport.subject=kradle.staging.events',
214
+ '--set', 'externalDependencies.nats.eventTransport.stream=KRADLE_STAGING_EVENTS',
215
+ '--set', 'externalDependencies.nats.eventTransport.requireBroker=true'
216
+ ], { encoding: 'utf8' });
217
+
218
+ assert.match(inline, /name: KRADLE_EVENT_TRANSPORT\s+value: "nats"/);
219
+ assert.match(inline, /name: KRADLE_EVENT_NATS_URL\s+value: "nats:\/\/nats\.kradle-system\.svc\.cluster\.local:4222"/);
220
+ assert.match(inline, /name: KRADLE_EVENT_NATS_SUBJECT\s+value: "kradle\.staging\.events"/);
221
+ assert.match(inline, /name: KRADLE_EVENT_NATS_STREAM\s+value: "KRADLE_STAGING_EVENTS"/);
222
+ assert.match(inline, /name: KRADLE_EVENT_REQUIRE_BROKER\s+value: "true"/);
223
+
224
+ const secretBacked = execFileSync('helm', [
225
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
226
+ '--set', 'argocd.enabled=false',
227
+ '--set', 'externalDependencies.nats.eventTransport.enabled=true',
228
+ '--set', 'externalDependencies.nats.existingSecret=kradle-nats',
229
+ '--set', 'externalDependencies.nats.key=connection-url'
230
+ ], { encoding: 'utf8' });
231
+
232
+ assert.match(secretBacked, /name: KRADLE_EVENT_NATS_URL[\s\S]*?secretKeyRef:[\s\S]*?name: "kradle-nats"[\s\S]*?key: "connection-url"[\s\S]*?optional: true/);
233
+ assert.doesNotMatch(secretBacked, /nats:\/\/[^"\s]+/);
234
+ });
235
+
236
+ test('demo NATS renders JetStream broker and wires event transport by default', () => {
237
+ const rendered = execFileSync('helm', [
238
+ 'template', 'kradle', '../charts', '-n', 'kradle-system',
239
+ '--set', 'argocd.enabled=false',
240
+ '--set', 'demo.enabled=true',
241
+ '--set', 'demo.nats.mode=local-dev-nats'
242
+ ], { encoding: 'utf8' });
243
+
244
+ assert.match(rendered, /name: kradle-kradle-nats/);
245
+ assert.match(rendered, /args: \["--jetstream", "--store_dir=\/data\/jetstream"\]/);
246
+ assert.match(rendered, /name: KRADLE_EVENT_TRANSPORT\s+value: "nats"/);
247
+ assert.match(rendered, /name: KRADLE_EVENT_NATS_URL\s+value: "nats:\/\/kradle-kradle-nats\.kradle-system\.svc\.cluster\.local:4222"/);
248
+ });
249
+
250
+ test('controller healthz returns deep dependency and event transport probe details', async () => {
251
+ const server = createKradleHttpServer({
252
+ controller: fixtureKubernetesController(),
253
+ healthProbeOptions: {
254
+ env: {
255
+ KRADLE_GITEA_HTTP_URL: 'https://gitea.internal',
256
+ AGENT_MUX_URL: 'https://mux.internal',
257
+ KRADLE_CONTROLLER_URL: 'https://controller.internal',
258
+ ANTHROPIC_API_KEY: 'sk-ant-api03-test-health-key',
259
+ KRADLE_KUBECTL: 'kubectl-test',
260
+ },
261
+ fetchImpl: async () => ({ ok: true, status: 200 }),
262
+ execFileImpl: async () => ({ stdout: 'Kubernetes control plane is running', stderr: '' }),
263
+ eventBus: {
264
+ status: () => ({ transport: 'nats-jetstream', status: 'ok', durable: true }),
265
+ },
266
+ },
267
+ });
268
+ await new Promise((resolve) => server.listen(0, resolve));
269
+ try {
270
+ const response = await fetch(`http://127.0.0.1:${server.address().port}/healthz`);
271
+ const body = await response.json();
272
+ assert.equal(response.status, 200);
273
+ assert.equal(body.ok, true);
274
+ assert.equal(body.status, 'ok');
275
+ assert.equal(body.health.gitea, 'ok');
276
+ assert.equal(body.health.agentMux, 'ok');
277
+ assert.equal(body.health.controller, 'ok');
278
+ assert.equal(body.health.eventTransport, 'ok');
279
+ assert.equal(body.health.details.eventTransport.transport, 'nats-jetstream');
280
+ assert.doesNotMatch(JSON.stringify(body), /sk-ant-api03-test-health-key/);
281
+ } finally {
282
+ await new Promise((resolve) => server.close(resolve));
283
+ }
284
+ });
285
+
286
+
287
+ test('web UI and controller API expose live Kubernetes deployment and publishing metadata', async () => {
288
+ const controller = fixtureKubernetesController();
289
+ const model = createControllerUiModel(await controller.snapshot());
290
+ assert.equal(model.controller.mode, 'kradle-workspace');
291
+ assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/controller?org=:org'));
292
+ assert.ok(model.controller.endpoints.some((endpoint) => endpoint.path === '/api/orgs/:org/resources'));
293
+ assert.equal(model.controller.architecture.apiController.role, 'kradle-api-controller');
294
+ assert.equal(model.controller.architecture.resourceGateway.role, 'kradle-resource-gateway');
295
+ assert.equal(model.controller.architecture.resourceClient.role, 'kradle-resource-client');
296
+ assert.equal(model.controller.architecture.repositoryService.role, 'repository-service');
297
+ assert.deepEqual(model.controller.architecture.apiController.delegatesTo, ['kradle-resource-gateway', 'repository-service']);
298
+ assert.ok(model.resources.some((resource) => resource.kind === 'Repository' && resource.count > 0));
299
+ assert.ok(model.resources.some((resource) => resource.kind === 'KradleProject'));
300
+ assert.ok(model.views.dashboard.issueSync?.gitea?.repo, 'dashboard exposes issue sync backend plan');
301
+ assert.equal(model.policyEngine.health, 'ready');
302
+ assert.equal(model.policyEngine.violations.length, 1);
303
+ assert.ok(model.resources.some((resource) => resource.kind === 'PolicyBinding' && resource.count > 0));
304
+ assert.ok(model.resources.find((resource) => resource.kind === 'Repository').action.list.includes('Open Repository records'));
305
+ assert.match(model.operations.image, /kradle-controller/);
306
+ assert.equal(model.operations.chart, 'charts/kradle');
307
+ for (const gate of ['npm run check', 'docker build', 'helm package charts/kradle', 'npm pack --json']) assert.ok(model.operations.releaseGates.includes(gate), `release gate ${gate}`);
308
+ assert.ok(model.validation.every((item) => typeof item.evidence === 'string' && item.evidence.length > 0));
309
+
310
+ const server = createKradleHttpServer({ controller });
311
+ await new Promise((resolve) => server.listen(0, resolve));
312
+ try {
313
+ const response = await fetch(`http://127.0.0.1:${server.address().port}/api/controller`);
314
+ assert.equal(response.status, 200);
315
+ const body = await response.json();
316
+ assert.deepEqual(body.operations.releaseGates, model.operations.releaseGates);
317
+ assert.equal(body.metrics.resources, model.metrics.resources);
318
+ } finally {
319
+ await new Promise((resolve) => server.close(resolve));
320
+ }
321
+ });
322
+
323
+ test('web UI is wired to the Kubernetes controller API instead of a static local snapshot', () => {
324
+ const page = read('../web/app/page.jsx');
325
+ const orgPage = read('../web/app/orgs/[org]/page.jsx');
326
+ const shellModules = ['../web/app/lib/kradle-ui.jsx', '../web/app/lib/page-frame.jsx', '../web/app/pages/agent-pages.jsx', '../web/app/pages/agent-helpers.jsx', '../web/app/pages/repo-pages.jsx', '../web/app/pages/manage-pages.jsx', '../web/app/pages/settings-pages.jsx', '../web/app/pages/external-pages.jsx'];
327
+ const shell = shellModules.map((m) => { try { return readFileSync(m, 'utf8'); } catch { return ''; } }).join('\n');
328
+ const actions = read('../web/app/components/resource-actions.jsx');
329
+ const client = read('src/controller-client.js');
330
+ const apiController = read('src/api-controller.js');
331
+ const gateway = read('src/kubernetes-resource-gateway.js');
332
+ const kubernetes = read('src/kubernetes-controller.js');
333
+ const server = read('src/http-server.js');
334
+ const webControllerRoute = read('../web/app/api/controller/route.js');
335
+ assert.ok(page.includes("redirect('/orgs/'"));
336
+ assert.ok(orgPage.includes('DashboardPage'));
337
+ assert.ok(read('../web/app/orgs/page.jsx').includes('Choose an organization'));
338
+ assert.ok(client.includes('KRADLE_CONTROLLER_URL'));
339
+ assert.ok(client.includes('KRADLE_CONTROLLER_REQUEST_TIMEOUT_MS'));
340
+ assert.ok(client.includes('AbortSignal.timeout'));
341
+ assert.ok(client.includes('if (!useCache) return revalidateFn();'));
342
+ assert.ok(client.includes('staleWhileRevalidate(organization, revalidateFn, swrOptions)'));
343
+ assert.ok(client.includes('getControllerSnapshotAsync'));
344
+ assert.ok(client.includes('fallbackSnapshot'));
345
+ assert.ok(!client.includes('createKubernetesResourceGateway'));
346
+ assert.ok(!client.includes('createKradleApiController'));
347
+ assert.ok(apiController.includes('resourceGateway'));
348
+ assert.ok(apiController.includes('withArchitecture'));
349
+ assert.ok(apiController.includes('kubernetes-resource-gateway'));
350
+ assert.ok(!apiController.includes('createKubernetesController'));
351
+ assert.ok(gateway.includes('createKubernetesResourceClient'));
352
+ assert.ok(kubernetes.includes('createKubernetesController(options = {})')); // resource-client alias
353
+ assert.ok(kubernetes.includes('/var/run/secrets/kubernetes.io/serviceaccount'), 'Kubernetes client can use in-cluster service-account credentials');
354
+ assert.ok(gateway.includes('repositoryManifest'));
355
+ assert.ok(shell.includes('/api/controller'));
356
+ assert.ok(webControllerRoute.includes('KRADLE_CONTROLLER_URL'));
357
+ assert.ok(webControllerRoute.includes('hydrateOrgResourceSummaries'), 'web API route hydrates empty controller summaries from org-scoped resources');
358
+ assert.ok(shell.includes('ArchitectureMap'));
359
+ assert.ok(shell.includes('Repository home'));
360
+ assert.ok(shell.includes('IssueWorkspace'));
361
+ assert.ok(shell.includes('IssueViewSwitcher'));
362
+ assert.ok(shell.includes('issuesForScope'));
363
+ assert.ok(shell.includes('issueRepositoryRefs'));
364
+ assert.ok(shell.includes('issueProjectRefs'));
365
+ assert.ok(shell.includes('DeploymentCenter'));
366
+ assert.ok(shell.includes('DeploymentManager'));
367
+ assert.ok(shell.includes('Kradle deployment center'));
368
+ assert.ok(shell.includes('PlanCard'));
369
+ assert.ok(shell.includes('Advanced resource details'));
370
+ assert.ok(shell.includes('Releases'));
371
+ assert.ok(shell.includes('managed resources'));
372
+ assert.ok(shell.includes('Advanced resource records'));
373
+ assert.ok(shell.includes('Deployments'));
374
+ assert.ok(actions.includes('Create deployment'));
375
+ assert.ok(actions.includes('Prepare deployment'));
376
+ assert.ok(shell.includes('environments'));
377
+ assert.ok(shell.includes('Repository code browser'));
378
+ assert.ok(shell.includes('Manage access'));
379
+ assert.ok(shell.includes('PeopleAdmin'));
380
+ assert.ok(shell.includes('getSignedInUser'));
381
+ assert.ok(shell.includes('topbarAccount'));
382
+ assert.ok(shell.includes('Signed-in user'));
383
+ assert.ok(shell.includes('Sign out'));
384
+ assert.ok(shell.includes('Invite people'));
385
+ assert.ok(shell.includes('identity links'));
386
+ assert.ok(shell.includes('repository permissions'));
387
+ assert.ok(shell.includes('Access readiness'));
388
+ assert.match(actions, /Mark accepted/i);
389
+ assert.ok(actions.includes('Revoke invite'));
390
+ assert.ok(actions.includes('Disable user'));
391
+ assert.ok(actions.includes('Restore user'));
392
+ assert.ok(actions.includes('Revoke grant'));
393
+ assert.ok(actions.includes('SSH keys'));
394
+ assert.ok(actions.includes('Save SSH key'));
395
+ assert.ok(actions.includes('Revoke SSH key'));
396
+ assert.equal(existsSync('../web/app/orgs/[org]/people/page.jsx'), true);
397
+ assert.equal(existsSync('../web/app/login/page.jsx'), true);
398
+ assert.equal(existsSync('../web/app/logout/page.jsx'), true);
399
+ assert.equal(existsSync('../web/app/orgs/[org]/runs/page.jsx'), true);
400
+ assert.equal(existsSync('../web/app/runs/page.jsx'), false);
401
+ assert.equal(existsSync('../web/app/pipelines/page.jsx'), false);
402
+ assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/runs/page.jsx'), true);
403
+ assert.equal(existsSync('../web/app/orgs/[org]/repositories/[repo]/issues/[issue]/page.jsx'), true);
404
+ assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/page.jsx'), true);
405
+ assert.equal(existsSync('../web/app/orgs/[org]/agents/projects/[projectId]/issues/[issue]/page.jsx'), true);
406
+ assert.equal(existsSync('../web/app/repositories/[repo]/runs/page.jsx'), false);
407
+ assert.equal(existsSync('../web/app/repositories/[repo]/pipelines/page.jsx'), false);
408
+ assert.equal(existsSync('../web/proxy.js'), true);
409
+ assert.equal(existsSync('../web/app/api/auth/[provider]/route.js'), true);
410
+ assert.equal(existsSync('../web/app/api/auth/callback/[provider]/route.js'), true);
411
+ assert.equal(existsSync('../web/app/api/auth/delegated/route.js'), true);
412
+ assert.ok(shell.includes('RepositoryCodePage'));
413
+ assert.ok(shell.includes('RepositoryPullRequestsPage'));
414
+ assert.ok(shell.includes('RepositorySettingsPage'));
415
+ assert.ok(shell.includes('PullRequestReviewPanel'));
416
+ assert.ok(shell.includes('RunCenter'));
417
+ assert.ok(shell.includes('Workspace runs'));
418
+ assert.ok(shell.includes('Run event stream'));
419
+ assert.ok(shell.includes('/runs'));
420
+ assert.ok(!shell.includes("['/pipelines', 'Runs']"));
421
+ assert.ok(!shell.includes('PipelinesPage'), 'legacy pipelines UI component is not exported');
422
+ assert.ok(!shell.includes('RepositoryPipelinesPage'), 'legacy repository pipelines UI component is not exported');
423
+ assert.ok(!shell.includes('function PipelineDebugger'), 'runs page does not use the old debugger-first panel');
424
+ assert.ok(shell.includes('Automation inspector'));
425
+ assert.ok(shell.includes('Readiness checklist'));
426
+ assert.ok(kubernetes.includes('spawnSync'));
427
+ assert.ok(kubernetes.includes('kubectl'));
428
+ assert.ok(!client.includes('createKradleUiDemoRuntime'));
429
+ assert.ok(!page.includes('createKradleMvpDemo'));
430
+ assert.ok(!page.includes('createKradleLifecycleSnapshot'));
431
+ assert.ok(apiController.includes('const repoPath = `/orgs/${encodeURIComponent(org)}/repositories/${encodeURIComponent(name)}`'));
432
+ assert.ok(apiController.includes('runs: `${repoPath}/runs`'));
433
+ assert.ok(server.includes('/api/controller'));
434
+ assert.ok(server.includes('orgResourceCollectionMatch'));
435
+ assert.ok(!server.includes("url.pathname === '/api/repositories'"));
436
+ });
437
+
438
+ test('API controller delegates through resource gateway instead of owning Kubernetes scope', async () => {
439
+ const calls = [];
440
+ const resourceGateway = {
441
+ namespace: 'kradle-org-default',
442
+ resourceDefinitions: [],
443
+ async snapshot() { calls.push('snapshot'); return { source: 'kubernetes', namespace: 'kradle-org-default', resources: {}, kubectl: { available: true, errors: [] }, commands: [], events: [], permissions: [], storage: {} }; },
444
+ async list(kind) { calls.push(`list:${kind}`); return { kind, items: [] }; },
445
+ async get(kind, name) { calls.push(`get:${kind}/${name}`); return { kind, metadata: { name } }; },
446
+ async apply(resource) { calls.push(`apply:${resource.kind}`); return { operation: 'apply', resource }; },
447
+ async delete(kind, name) { calls.push(`delete:${kind}/${name}`); return { operation: 'delete', kind, name }; },
448
+ async createRepository(input) { calls.push(`createRepository:${input.name}`); return { operation: 'apply', resource: { kind: 'Repository', metadata: { name: input.name } } }; },
449
+ async createOrganization(input) { calls.push(`createOrganization:${input.name}`); return { operation: 'create-organization', organization: { metadata: { name: input.name } } }; },
450
+ watch(resourcePath) { calls.push(`watch:${resourcePath}`); return { child: { kill() {} } }; }
451
+ };
452
+ const controller = createKradleApiController({ resourceGateway });
453
+ await controller.snapshot();
454
+ await controller.listResource('Repository');
455
+ await controller.getResource('Repository', 'app');
456
+ await controller.applyResource({ kind: 'Repository', metadata: { name: 'app' } });
457
+ await controller.deleteResource('Repository', 'app');
458
+ await controller.createRepository({ name: 'next-app' });
459
+ await controller.createOrganization({ name: 'product' });
460
+ controller.watchResource('orgs/default/repositories');
461
+ assert.deepEqual(calls, ['snapshot', 'list:Repository', 'get:Repository/app', 'apply:Repository', 'delete:Repository/app', 'createRepository:next-app', 'createOrganization:product', 'watch:orgs/default/repositories']);
462
+ assert.equal(controller.resourceGateway, resourceGateway);
463
+ });
464
+
465
+ test('GitHub workflow publishes deployable image and chart artifacts with safe gates', () => {
466
+ const workflow = read('.github/workflows/publish.yml');
467
+ const validate = workflowJobBlock(workflow, 'validate');
468
+ assert.ok(workflow.includes('branches: [develop, staging, main]'));
469
+ assert.ok(workflow.includes('concurrency:') && workflow.includes('group: publish-${{ github.ref_name }}'));
470
+ assert.ok(validate.includes('npm run check'));
471
+ assert.ok(validate.includes('npm pack --json'));
472
+ assert.ok(validate.includes('name: npm-package'));
473
+ assert.ok(validate.includes('name: dist-artifacts'));
474
+ assert.ok(validate.includes('name: ui-standalone'));
475
+ assert.ok(validate.includes('Generate release checksums'));
476
+ assert.ok(validate.includes('sha256sum'));
477
+ assert.ok(validate.includes('name: release-checksums'));
478
+ assert.ok(read('../charts/templates/gitea.yaml').includes('gitea-backend'));
479
+ const argocdTemplate = read('../charts/templates/argocd-application.yaml');
480
+ const values = read('../charts/values.yaml');
481
+ assert.ok(argocdTemplate.includes('kind: Application'));
482
+ assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.prune'));
483
+ assert.ok(argocdTemplate.includes('.Values.argocd.syncPolicy.selfHeal'));
484
+ assert.ok(!values.includes('`n'));
485
+ assert.match(values, /^ syncPolicy:\r?\n\s+automated: true\r?\n\s+prune: true\r?\n\s+selfHeal: true/m);
486
+
487
+ const image = workflowJobBlock(workflow, 'publish-image');
488
+ assert.ok(image.includes('needs: validate'));
489
+ assert.ok(image.includes("push: ${{ github.event_name != 'pull_request' }}"));
490
+ assert.ok(image.includes('docker/build-push-action'));
491
+ assert.ok(image.includes('ghcr.io/${{ github.repository }}/kradle-controller'));
492
+
493
+ const chart = workflowJobBlock(workflow, 'publish-chart');
494
+ assert.ok(chart.includes('needs: validate'));
495
+ assert.ok(chart.includes('helm package charts/kradle'));
496
+ assert.ok(chart.includes('SHA256SUMS'));
497
+ assert.ok(chart.includes('sha256sum dist/charts/*.tgz'));
498
+ assert.ok(chart.includes("if: startsWith(github.ref, 'refs/tags/v')"));
499
+ assert.ok(chart.includes('helm push dist/charts/*.tgz'));
500
+
501
+ const deploy = workflowJobBlock(workflow, 'deploy-kradle');
502
+ assert.ok(deploy.includes('Deploy Kradle To AKS'));
503
+ assert.ok(deploy.includes("github.ref == 'refs/heads/develop'"));
504
+ assert.ok(deploy.includes("github.ref == 'refs/heads/staging'"));
505
+ assert.ok(deploy.includes("github.ref == 'refs/heads/main'"));
506
+ assert.ok(deploy.includes('environment:') && deploy.includes('kradle-production') && deploy.includes('https://kradle.a5c.ai'));
507
+ assert.ok(deploy.includes('AZURE_ACR_NAME') && deploy.includes('KUBE_CONFIG'));
508
+ assert.ok(deploy.includes('KRADLE_GITHUB_CLIENT_ID') && deploy.includes('KRADLE_GITHUB_CLIENT_SECRET'));
509
+ assert.ok(deploy.includes('kradle-develop.a5c.ai') && deploy.includes('kradle-staging.a5c.ai') && deploy.includes('kradle.a5c.ai'));
510
+ assert.ok(deploy.includes('docker build -f Dockerfile') && deploy.includes('docker push'));
511
+ assert.ok(deploy.includes('create secret docker-registry acr-pull'));
512
+ assert.ok(deploy.includes('helm upgrade --install'));
513
+ assert.ok(deploy.includes('--values /tmp/kradle-deploy-values.yaml'));
514
+ assert.ok(deploy.includes('--wait'));
515
+ assert.ok(deploy.includes('rollout status deployment/"${HELM_RELEASE}-kradle-web"'));
516
+ assert.doesNotMatch(workflow, /publish-npm:/);
517
+ assert.doesNotMatch(workflow, /npm publish/);
518
+ assert.doesNotMatch(workflow, /PUBLISH_NPM/);
519
+ });
520
+
521
+
522
+
523
+
524
+
525
+
526
+
527
+