@a5c-ai/krate 5.0.1-staging.00fa5317c

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 (256) hide show
  1. package/Dockerfile +31 -0
  2. package/README.md +183 -0
  3. package/bin/krate-demo.mjs +23 -0
  4. package/bin/krate-server.mjs +14 -0
  5. package/dist/krate-controller-ui.json +3205 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +3125 -0
  8. package/dist/krate-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-krate-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/components/control-plane.md +78 -0
  60. package/docs/components/data-plane.md +69 -0
  61. package/docs/components/hooks-events.md +67 -0
  62. package/docs/components/identity-rbac-policy.md +73 -0
  63. package/docs/components/kubevela-oam.md +70 -0
  64. package/docs/components/operations-publishing.md +81 -0
  65. package/docs/components/runners-ci.md +66 -0
  66. package/docs/components/web-ui.md +94 -0
  67. package/docs/external/README.md +47 -0
  68. package/docs/external/bidirectional-sync-design.md +134 -0
  69. package/docs/external/cicd-interface.md +64 -0
  70. package/docs/external/external-backend-controllers.md +170 -0
  71. package/docs/external/external-backend-crds.md +234 -0
  72. package/docs/external/external-backend-ui-spec.md +151 -0
  73. package/docs/external/external-backend-ux-flows.md +115 -0
  74. package/docs/external/external-object-mapping.md +125 -0
  75. package/docs/external/git-forge-interface.md +68 -0
  76. package/docs/external/github-integration-design.md +151 -0
  77. package/docs/external/issue-tracking-interface.md +66 -0
  78. package/docs/external/provider-capability-manifests.md +204 -0
  79. package/docs/external/provider-catalog.md +139 -0
  80. package/docs/external/provider-rollout-testing.md +78 -0
  81. package/docs/external/research-results.md +48 -0
  82. package/docs/external/security-auth-permissions.md +81 -0
  83. package/docs/external/sync-state-machines.md +108 -0
  84. package/docs/external/unified-external-backend-model.md +107 -0
  85. package/docs/external/user-facing-changes.md +67 -0
  86. package/docs/gaps.md +161 -0
  87. package/docs/install.md +94 -0
  88. package/docs/krate-design.md +334 -0
  89. package/docs/local-minikube.md +55 -0
  90. package/docs/ontology/README.md +32 -0
  91. package/docs/ontology/bounded-contexts.md +29 -0
  92. package/docs/ontology/events-and-hooks.md +32 -0
  93. package/docs/ontology/oam-kubevela.md +32 -0
  94. package/docs/ontology/operations-and-release.md +25 -0
  95. package/docs/ontology/personas-and-actors.md +32 -0
  96. package/docs/ontology/policies-and-invariants.md +33 -0
  97. package/docs/ontology/problem-space.md +30 -0
  98. package/docs/ontology/resource-contracts.md +40 -0
  99. package/docs/ontology/resource-taxonomy.md +42 -0
  100. package/docs/ontology/runners-and-ci.md +29 -0
  101. package/docs/ontology/solution-space.md +24 -0
  102. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  103. package/docs/ontology/validation-matrix.md +24 -0
  104. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  105. package/docs/ontology/workflows.md +39 -0
  106. package/docs/ontology/world.md +35 -0
  107. package/docs/openapi.yaml +1275 -0
  108. package/docs/product-requirements.md +62 -0
  109. package/docs/roadmap-mvp.md +87 -0
  110. package/docs/system-requirements.md +90 -0
  111. package/docs/tests/README.md +53 -0
  112. package/docs/tests/agent-qa-plan.md +63 -0
  113. package/docs/tests/browser-ui-tests.md +62 -0
  114. package/docs/tests/ci-quality-gates.md +48 -0
  115. package/docs/tests/coverage-model.md +64 -0
  116. package/docs/tests/e2e-scenario-tests.md +53 -0
  117. package/docs/tests/fixtures-test-data.md +63 -0
  118. package/docs/tests/observability-reliability-tests.md +54 -0
  119. package/docs/tests/product-test-matrix.md +145 -0
  120. package/docs/tests/qa-adoption-roadmap.md +130 -0
  121. package/docs/tests/qa-automation-plan.md +101 -0
  122. package/docs/tests/security-compliance-tests.md +57 -0
  123. package/docs/tests/test-framework-tools.md +88 -0
  124. package/docs/tests/test-suite-layout.md +121 -0
  125. package/docs/tests/unit-integration-tests.md +48 -0
  126. package/docs/todo-kyverno +714 -0
  127. package/docs/todos.md +4 -0
  128. package/docs/user-stories.md +78 -0
  129. package/examples/minikube-demo.yaml +190 -0
  130. package/examples/oam-application.yaml +23 -0
  131. package/examples/policy-kyverno-pr-title.yaml +18 -0
  132. package/package.json +63 -0
  133. package/scripts/build.mjs +29 -0
  134. package/scripts/setup-minikube.mjs +65 -0
  135. package/scripts/smoke.mjs +37 -0
  136. package/scripts/validate-doc-coverage.mjs +152 -0
  137. package/scripts/validate-package.mjs +93 -0
  138. package/scripts/validate-ui.mjs +278 -0
  139. package/src/agent-adapter-controller.js +169 -0
  140. package/src/agent-approval-controller.js +170 -0
  141. package/src/agent-context-bundles.js +242 -0
  142. package/src/agent-dispatch-controller.js +209 -0
  143. package/src/agent-gateway-config-controller.js +147 -0
  144. package/src/agent-memory-controller.js +357 -0
  145. package/src/agent-memory-import.js +327 -0
  146. package/src/agent-memory-query.js +292 -0
  147. package/src/agent-memory-repository-source-controller.js +255 -0
  148. package/src/agent-mux-client.js +280 -0
  149. package/src/agent-permission-review.js +250 -0
  150. package/src/agent-project-controller.js +117 -0
  151. package/src/agent-provider-config-controller.js +150 -0
  152. package/src/agent-secret-config-grant-controller.js +282 -0
  153. package/src/agent-session-transcript-controller.js +189 -0
  154. package/src/agent-stack-controller.js +347 -0
  155. package/src/agent-subagent-controller.js +160 -0
  156. package/src/agent-transport-binding-controller.js +121 -0
  157. package/src/agent-trigger-controller.js +381 -0
  158. package/src/agent-workspace-controller.js +702 -0
  159. package/src/agent-writeback-controller.js +302 -0
  160. package/src/api-controller.js +541 -0
  161. package/src/argocd-gitops.js +43 -0
  162. package/src/async-controller.js +207 -0
  163. package/src/audit-controller.js +191 -0
  164. package/src/auth.js +307 -0
  165. package/src/component-catalog.js +41 -0
  166. package/src/control-plane.js +136 -0
  167. package/src/controller-client.js +72 -0
  168. package/src/controller-ui.js +617 -0
  169. package/src/data-plane.js +179 -0
  170. package/src/event-bus.js +61 -0
  171. package/src/external/conflict-controller.js +225 -0
  172. package/src/external/github/auth.js +96 -0
  173. package/src/external/github/cicd.js +180 -0
  174. package/src/external/github/git-forge.js +240 -0
  175. package/src/external/github/index.js +144 -0
  176. package/src/external/github/issue-tracking.js +163 -0
  177. package/src/external/provider-adapter.js +161 -0
  178. package/src/external/provider-resource-factory.js +161 -0
  179. package/src/external/sync-controller.js +235 -0
  180. package/src/external/webhook-controller.js +144 -0
  181. package/src/external/write-controller.js +283 -0
  182. package/src/gitea-backend.js +131 -0
  183. package/src/gitea-service.js +173 -0
  184. package/src/handoff.js +98 -0
  185. package/src/hooks-events.js +63 -0
  186. package/src/http-server.js +377 -0
  187. package/src/identity-policy.js +86 -0
  188. package/src/index.js +57 -0
  189. package/src/kubernetes-controller-async.js +511 -0
  190. package/src/kubernetes-controller.js +878 -0
  191. package/src/kubernetes-resource-gateway.js +48 -0
  192. package/src/notification-controller.js +178 -0
  193. package/src/operations.js +112 -0
  194. package/src/org-scoping.js +5 -0
  195. package/src/resource-model.js +221 -0
  196. package/src/runner-controller.js +272 -0
  197. package/src/runners-ci.js +48 -0
  198. package/src/runtime.js +196 -0
  199. package/src/snapshot-cache.js +157 -0
  200. package/src/web-ui.js +40 -0
  201. package/tests/agent-adapter-controller.test.js +361 -0
  202. package/tests/agent-approval-controller.test.js +173 -0
  203. package/tests/agent-context-bundles.test.js +278 -0
  204. package/tests/agent-dispatch-controller.test.js +315 -0
  205. package/tests/agent-gateway-config-controller.test.js +386 -0
  206. package/tests/agent-memory-controller.test.js +308 -0
  207. package/tests/agent-memory-import-snapshot.test.js +477 -0
  208. package/tests/agent-memory-query.test.js +404 -0
  209. package/tests/agent-memory-repository-source.test.js +514 -0
  210. package/tests/agent-mux-client.test.js +204 -0
  211. package/tests/agent-permission-review-v2.test.js +317 -0
  212. package/tests/agent-permission-review.test.js +209 -0
  213. package/tests/agent-project-controller.test.js +302 -0
  214. package/tests/agent-provider-config-controller.test.js +376 -0
  215. package/tests/agent-resources.test.js +228 -0
  216. package/tests/agent-secret-config-grant.test.js +231 -0
  217. package/tests/agent-session-transcript-controller.test.js +499 -0
  218. package/tests/agent-stack-controller.test.js +221 -0
  219. package/tests/agent-subagent-controller.test.js +201 -0
  220. package/tests/agent-transport-binding-controller.test.js +294 -0
  221. package/tests/agent-trigger-controller.test.js +211 -0
  222. package/tests/agent-trigger-routes.test.js +190 -0
  223. package/tests/agent-trigger-sources.test.js +245 -0
  224. package/tests/agent-workspace-controller.test.js +181 -0
  225. package/tests/agent-writeback.test.js +292 -0
  226. package/tests/approval-persistence.test.js +171 -0
  227. package/tests/async-controller.test.js +252 -0
  228. package/tests/audit-controller.test.js +227 -0
  229. package/tests/codespace-controller.test.js +318 -0
  230. package/tests/deployment.test.js +407 -0
  231. package/tests/e2e/lifecycle.test.js +117 -0
  232. package/tests/event-bus-integration.test.js +190 -0
  233. package/tests/external-github-forge.test.js +560 -0
  234. package/tests/external-github-issues-cicd.test.js +520 -0
  235. package/tests/external-integration.test.js +470 -0
  236. package/tests/external-persistence.test.js +340 -0
  237. package/tests/external-provider-adapter.test.js +365 -0
  238. package/tests/external-resource-model.test.js +215 -0
  239. package/tests/external-webhook-sync.test.js +287 -0
  240. package/tests/external-write-conflict.test.js +353 -0
  241. package/tests/gitea-service.test.js +253 -0
  242. package/tests/health-check-real.test.js +165 -0
  243. package/tests/integration/full-flow.test.js +266 -0
  244. package/tests/krate.test.js +756 -0
  245. package/tests/memory-search-wiring.test.js +270 -0
  246. package/tests/notification-controller.test.js +196 -0
  247. package/tests/notification-integration.test.js +179 -0
  248. package/tests/org-scoping.test.js +687 -0
  249. package/tests/runner-controller.test.js +327 -0
  250. package/tests/runner-integration.test.js +231 -0
  251. package/tests/session-cookie-hmac.test.js +151 -0
  252. package/tests/snapshot-performance.test.js +247 -0
  253. package/tests/sse-events.test.js +107 -0
  254. package/tests/webhook-trigger.test.js +198 -0
  255. package/tests/workspace-volumes.test.js +312 -0
  256. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,687 @@
1
+ // Slice 1.1 — Org Scoping Enforcement
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { describe, test } from 'node:test';
5
+ import {
6
+ orgNamespaceName,
7
+ normalizeOrgSlug,
8
+ createResource,
9
+ } from '../src/index.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helper: create a fake resource gateway for controller tests
13
+ // ---------------------------------------------------------------------------
14
+ function createFakeGateway(overrides = {}) {
15
+ const calls = { applied: [], deleted: [], got: [] };
16
+ return {
17
+ calls,
18
+ gateway: {
19
+ role: 'kubernetes-resource-gateway',
20
+ namespace: 'krate-system',
21
+ resourceDefinitions: [],
22
+ async snapshot() { return { source: 'kubernetes', namespace: 'krate-system', resources: {}, commands: [], events: [], permissions: [], storage: {} }; },
23
+ async list() { return overrides.listResult || { items: [] }; },
24
+ async get(kind, name) { calls.got.push({ kind, name }); return overrides.getResult !== undefined ? overrides.getResult : null; },
25
+ async apply(resource) { calls.applied.push(resource); return { operation: 'apply', resource }; },
26
+ async delete(kind, name) { calls.deleted.push({ kind, name }); return { operation: 'delete', resource: null }; },
27
+ async createRepository(input) { return { operation: 'apply', resource: input }; },
28
+ async createOrganization(input) { return { operation: 'create-organization', organization: { kind: 'Organization', metadata: { name: input.slug || input.name }, spec: { organizationRef: input.slug || input.name, ...input } } }; },
29
+ watch() { return {}; }
30
+ }
31
+ };
32
+ }
33
+
34
+ async function makeController(overrides = {}, controllerOptions = {}) {
35
+ const { calls, gateway } = createFakeGateway(overrides);
36
+ const { createKrateApiController } = await import('../src/api-controller.js');
37
+ const controller = createKrateApiController({ resourceGateway: gateway, ...controllerOptions });
38
+ return { controller, calls };
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Test 1 — Org namespace derivation
43
+ // ---------------------------------------------------------------------------
44
+ describe('Slice 1.1 — Org namespace derivation', () => {
45
+ test('orgNamespaceName derives krate-org-<slug> from org slug', () => {
46
+ return import('../src/org-scoping.js').then(({ orgNamespaceName: scopedFn }) => {
47
+ assert.equal(scopedFn('a5c-ai'), 'krate-org-a5c-ai');
48
+ assert.equal(scopedFn('my-org'), 'krate-org-my-org');
49
+ assert.equal(scopedFn('default'), 'krate-org-default');
50
+ });
51
+ });
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Test 2 — applyResourceForOrg creates resource in correct namespace
56
+ // ---------------------------------------------------------------------------
57
+ describe('Slice 1.1 — applyResource in org scope', () => {
58
+ test('applyResourceForOrg creates resource in the correct org namespace', async () => {
59
+ const { controller, calls } = await makeController();
60
+
61
+ assert.equal(typeof controller.applyResourceForOrg, 'function',
62
+ 'controller must expose applyResourceForOrg(orgSlug, resource)');
63
+
64
+ const resource = createResource('Repository', { name: 'my-repo' }, {
65
+ organizationRef: 'a5c-ai',
66
+ visibility: 'internal'
67
+ });
68
+
69
+ const result = await controller.applyResourceForOrg('a5c-ai', resource);
70
+
71
+ assert.equal(
72
+ result.resource.metadata.namespace,
73
+ 'krate-org-a5c-ai',
74
+ 'resource must be placed in the org namespace krate-org-a5c-ai'
75
+ );
76
+ assert.equal(calls.applied.length, 1);
77
+ assert.equal(calls.applied[0].metadata.namespace, 'krate-org-a5c-ai');
78
+ });
79
+ });
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Test 3 — Cross-org reference denial (krate-org-* namespace mismatch)
83
+ // ---------------------------------------------------------------------------
84
+ describe('Slice 1.1 — Cross-org reference denial', () => {
85
+ test('applyResource rejects a resource that references a different org namespace', async () => {
86
+ const { controller, calls } = await makeController();
87
+
88
+ const crossOrgResource = {
89
+ apiVersion: 'krate.a5c.ai/v1alpha1',
90
+ kind: 'Repository',
91
+ metadata: {
92
+ name: 'stolen-repo',
93
+ namespace: 'krate-org-org-b',
94
+ labels: {}
95
+ },
96
+ spec: {
97
+ organizationRef: 'org-a',
98
+ visibility: 'internal'
99
+ }
100
+ };
101
+
102
+ await assert.rejects(
103
+ () => controller.applyResource(crossOrgResource),
104
+ (err) => {
105
+ assert.match(
106
+ err.message,
107
+ /cross.org|org.*mismatch|namespace.*does not match/i,
108
+ 'error message must describe the cross-org violation'
109
+ );
110
+ return true;
111
+ },
112
+ 'applyResource must reject resources with cross-org namespace/organizationRef mismatch'
113
+ );
114
+
115
+ assert.equal(calls.applied.length, 0, 'gateway.apply must not be called for a cross-org resource');
116
+ });
117
+ });
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Test 4 — Org-scoped resource listing
121
+ // ---------------------------------------------------------------------------
122
+ describe('Slice 1.1 — Org-scoped resource listing', () => {
123
+ test('listResource with org filter returns only resources from that org namespace', async () => {
124
+ const repoOrgA = createResource(
125
+ 'Repository',
126
+ { name: 'repo-alpha', namespace: 'krate-org-org-a' },
127
+ { organizationRef: 'org-a', visibility: 'internal' }
128
+ );
129
+ const repoOrgB = createResource(
130
+ 'Repository',
131
+ { name: 'repo-beta', namespace: 'krate-org-org-b' },
132
+ { organizationRef: 'org-b', visibility: 'private' }
133
+ );
134
+
135
+ const { controller } = await makeController({ listResult: { items: [repoOrgA, repoOrgB] } });
136
+
137
+ assert.equal(typeof controller.listResourceForOrg, 'function',
138
+ 'controller must expose listResourceForOrg(org, kind)');
139
+
140
+ const result = await controller.listResourceForOrg('org-a', 'Repository');
141
+
142
+ assert.ok(Array.isArray(result.items), 'result must have an items array');
143
+ assert.equal(result.items.length, 1, 'only org-a resources should be returned');
144
+ assert.equal(result.items[0].metadata.name, 'repo-alpha');
145
+ assert.equal(
146
+ result.items.every((item) => item.spec?.organizationRef === 'org-a'),
147
+ true,
148
+ 'every returned item must belong to org-a'
149
+ );
150
+ });
151
+ });
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Test 5 — Org-scoped audit event
155
+ // ---------------------------------------------------------------------------
156
+ describe('Slice 1.1 — Org-scoped audit event', () => {
157
+ test('applyResource emits an audit event with org context after a successful apply', async () => {
158
+ const auditEvents = [];
159
+
160
+ const resource = createResource(
161
+ 'Repository',
162
+ { name: 'audited-repo', namespace: 'krate-org-a5c-ai' },
163
+ { organizationRef: 'a5c-ai', visibility: 'internal' }
164
+ );
165
+
166
+ const { controller } = await makeController({}, {
167
+ onAuditEvent: (event) => auditEvents.push(event)
168
+ });
169
+
170
+ await controller.applyResource(resource);
171
+
172
+ assert.equal(auditEvents.length, 1, 'exactly one audit event must be emitted per apply');
173
+
174
+ const event = auditEvents[0];
175
+
176
+ assert.ok(event, 'audit event must be emitted');
177
+ assert.equal(event.operation, 'apply', 'event.operation must be "apply"');
178
+ assert.equal(event.org, 'a5c-ai', 'event.org must be the org slug');
179
+ assert.equal(event.namespace, 'krate-org-a5c-ai', 'event.namespace must be the org namespace');
180
+ assert.equal(event.kind, 'Repository', 'event.kind must match the resource kind');
181
+ assert.equal(event.name, 'audited-repo', 'event.name must match the resource name');
182
+ assert.ok(event.timestamp, 'event.timestamp must be present');
183
+ assert.doesNotThrow(() => new Date(event.timestamp).toISOString(),
184
+ 'event.timestamp must be a valid ISO date string');
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Test 6 — applyResourceForOrg rejects org mismatch in organizationRef
190
+ // ---------------------------------------------------------------------------
191
+ describe('Slice 1.1 — applyResourceForOrg org mismatch error path', () => {
192
+ test('applyResourceForOrg rejects when organizationRef does not match target org', async () => {
193
+ const { controller, calls } = await makeController();
194
+
195
+ const resource = createResource('Repository', { name: 'mismatch-repo' }, {
196
+ organizationRef: 'org-a',
197
+ visibility: 'internal'
198
+ });
199
+
200
+ await assert.rejects(
201
+ () => controller.applyResourceForOrg('org-b', resource),
202
+ (err) => {
203
+ assert.match(
204
+ err.message,
205
+ /org.*mismatch/i,
206
+ 'error must describe the org mismatch'
207
+ );
208
+ return true;
209
+ }
210
+ );
211
+
212
+ assert.equal(calls.applied.length, 0, 'gateway.apply must not be called on org mismatch');
213
+ });
214
+ });
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Test 7 — deleteResourceForOrg happy path
218
+ // ---------------------------------------------------------------------------
219
+ describe('Slice 1.1 — deleteResourceForOrg', () => {
220
+ test('deleteResourceForOrg deletes resource in the correct org namespace', async () => {
221
+ const orgResource = {
222
+ resource: createResource('Repository', { name: 'my-repo', namespace: 'krate-org-org-a' }, {
223
+ organizationRef: 'org-a',
224
+ visibility: 'internal'
225
+ })
226
+ };
227
+
228
+ const { controller, calls } = await makeController({ getResult: orgResource });
229
+
230
+ assert.equal(typeof controller.deleteResourceForOrg, 'function',
231
+ 'controller must expose deleteResourceForOrg(org, kind, name)');
232
+
233
+ await controller.deleteResourceForOrg('org-a', 'Repository', 'my-repo');
234
+
235
+ assert.equal(calls.deleted.length, 1, 'gateway.delete must be called once');
236
+ assert.equal(calls.deleted[0].name, 'my-repo');
237
+ });
238
+ });
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Test 8 — deleteResourceForOrg cross-org denial
242
+ // ---------------------------------------------------------------------------
243
+ describe('Slice 1.1 — deleteResourceForOrg cross-org denial', () => {
244
+ test('deleteResourceForOrg rejects when resource belongs to a different org', async () => {
245
+ const foreignResource = {
246
+ resource: createResource('Repository', { name: 'foreign-repo', namespace: 'krate-org-org-b' }, {
247
+ organizationRef: 'org-b',
248
+ visibility: 'internal'
249
+ })
250
+ };
251
+
252
+ const { controller, calls } = await makeController({ getResult: foreignResource });
253
+
254
+ await assert.rejects(
255
+ () => controller.deleteResourceForOrg('org-a', 'Repository', 'foreign-repo'),
256
+ (err) => {
257
+ assert.match(
258
+ err.message,
259
+ /cross.org|does not match/i,
260
+ 'error must describe the cross-org denial'
261
+ );
262
+ return true;
263
+ }
264
+ );
265
+
266
+ assert.equal(calls.deleted.length, 0, 'gateway.delete must not be called for cross-org denial');
267
+ });
268
+ });
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Test 9 — getResourceForOrg happy path
272
+ // ---------------------------------------------------------------------------
273
+ describe('Slice 1.1 — getResourceForOrg', () => {
274
+ test('getResourceForOrg returns resource when org matches', async () => {
275
+ const orgResource = {
276
+ resource: createResource('Repository', { name: 'my-repo', namespace: 'krate-org-org-a' }, {
277
+ organizationRef: 'org-a',
278
+ visibility: 'internal'
279
+ })
280
+ };
281
+
282
+ const { controller } = await makeController({ getResult: orgResource });
283
+
284
+ assert.equal(typeof controller.getResourceForOrg, 'function',
285
+ 'controller must expose getResourceForOrg(org, kind, name)');
286
+
287
+ const result = await controller.getResourceForOrg('org-a', 'Repository', 'my-repo');
288
+
289
+ assert.ok(result, 'result must be returned');
290
+ assert.equal(result.resource.metadata.name, 'my-repo');
291
+ });
292
+ });
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Test 10 — getResourceForOrg cross-org denial
296
+ // ---------------------------------------------------------------------------
297
+ describe('Slice 1.1 — getResourceForOrg cross-org denial', () => {
298
+ test('getResourceForOrg rejects when resource belongs to a different org', async () => {
299
+ const foreignResource = {
300
+ resource: createResource('Repository', { name: 'stolen-repo', namespace: 'krate-org-org-b' }, {
301
+ organizationRef: 'org-b',
302
+ visibility: 'internal'
303
+ })
304
+ };
305
+
306
+ const { controller } = await makeController({ getResult: foreignResource });
307
+
308
+ await assert.rejects(
309
+ () => controller.getResourceForOrg('org-a', 'Repository', 'stolen-repo'),
310
+ (err) => {
311
+ assert.match(
312
+ err.message,
313
+ /cross.org|does not match/i,
314
+ 'error must describe the cross-org denial'
315
+ );
316
+ return true;
317
+ }
318
+ );
319
+ });
320
+ });
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Test 11 — createRepository emits audit event
324
+ // ---------------------------------------------------------------------------
325
+ describe('Slice 1.1 — createRepository audit', () => {
326
+ test('createRepository emits an audit event', async () => {
327
+ const auditEvents = [];
328
+
329
+ const { controller } = await makeController({}, {
330
+ onAuditEvent: (event) => auditEvents.push(event)
331
+ });
332
+
333
+ await controller.createRepository({
334
+ name: 'new-repo',
335
+ organizationRef: 'a5c-ai',
336
+ visibility: 'internal'
337
+ });
338
+
339
+ assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
340
+ const event = auditEvents.find((e) => e.operation === 'create-repository');
341
+ assert.ok(event, 'audit event with operation "create-repository" must exist');
342
+ assert.ok(event.timestamp, 'event.timestamp must be present');
343
+ });
344
+ });
345
+
346
+ // ---------------------------------------------------------------------------
347
+ // Test 12 — createOrganization emits audit event
348
+ // ---------------------------------------------------------------------------
349
+ describe('Slice 1.1 — createOrganization audit', () => {
350
+ test('createOrganization emits an audit event', async () => {
351
+ const auditEvents = [];
352
+
353
+ const { controller } = await makeController({}, {
354
+ onAuditEvent: (event) => auditEvents.push(event)
355
+ });
356
+
357
+ await controller.createOrganization({
358
+ slug: 'new-org',
359
+ displayName: 'New Org'
360
+ });
361
+
362
+ assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
363
+ const event = auditEvents.find((e) => e.operation === 'create-organization');
364
+ assert.ok(event, 'audit event with operation "create-organization" must exist');
365
+ assert.ok(event.timestamp, 'event.timestamp must be present');
366
+ });
367
+ });
368
+
369
+ // ---------------------------------------------------------------------------
370
+ // Test 13 — Audit callback error resilience
371
+ // ---------------------------------------------------------------------------
372
+ describe('Slice 1.1 — Audit callback error resilience', () => {
373
+ test('audit callback error does not crash apply operations', async () => {
374
+ const { controller } = await makeController({}, {
375
+ onAuditEvent: () => { throw new Error('audit system down'); }
376
+ });
377
+
378
+ const resource = createResource('Repository', { name: 'resilient-repo', namespace: 'krate-org-a5c-ai' }, {
379
+ organizationRef: 'a5c-ai',
380
+ visibility: 'internal'
381
+ });
382
+
383
+ // This must not throw despite the audit callback throwing
384
+ const result = await controller.applyResource(resource);
385
+ assert.ok(result, 'apply must succeed even when audit callback throws');
386
+ assert.equal(result.operation, 'apply');
387
+ });
388
+ });
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Test 14 — applyResourceForOrg normalizes un-normalized slug
392
+ // ---------------------------------------------------------------------------
393
+ describe('Slice 1.1 — applyResourceForOrg slug normalization', () => {
394
+ test('applyResourceForOrg with un-normalized slug ("Org-A") normalizes the stored organizationRef to "org-a"', async () => {
395
+ const { controller, calls } = await makeController();
396
+
397
+ const resource = createResource('Repository', { name: 'norm-repo' }, {
398
+ visibility: 'internal'
399
+ });
400
+
401
+ const result = await controller.applyResourceForOrg('Org-A', resource);
402
+
403
+ assert.equal(result.resource.spec.organizationRef, 'org-a',
404
+ 'organizationRef must be normalized to lowercase');
405
+ assert.equal(result.resource.metadata.namespace, 'krate-org-org-a',
406
+ 'namespace must use the normalized slug');
407
+ assert.equal(calls.applied.length, 1);
408
+ assert.equal(calls.applied[0].spec.organizationRef, 'org-a');
409
+ });
410
+ });
411
+
412
+ // ---------------------------------------------------------------------------
413
+ // Test 15 — deleteResourceForOrg when resource has no namespace
414
+ // ---------------------------------------------------------------------------
415
+ describe('Slice 1.1 — deleteResourceForOrg no-namespace denial', () => {
416
+ test('deleteResourceForOrg when resource has no namespace throws cross-org error', async () => {
417
+ const noNsResource = {
418
+ resource: createResource('Repository', { name: 'no-ns-repo' }, {
419
+ organizationRef: 'org-a',
420
+ visibility: 'internal'
421
+ })
422
+ };
423
+ // Ensure metadata.namespace is absent
424
+ delete noNsResource.resource.metadata.namespace;
425
+
426
+ const { controller, calls } = await makeController({ getResult: noNsResource });
427
+
428
+ await assert.rejects(
429
+ () => controller.deleteResourceForOrg('org-a', 'Repository', 'no-ns-repo'),
430
+ (err) => {
431
+ assert.match(err.message, /cross.org|does not match/i,
432
+ 'error must describe cross-org denial for absent namespace');
433
+ return true;
434
+ }
435
+ );
436
+
437
+ assert.equal(calls.deleted.length, 0, 'gateway.delete must not be called');
438
+ });
439
+ });
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // Test 16 — getResourceForOrg when resource has no namespace
443
+ // ---------------------------------------------------------------------------
444
+ describe('Slice 1.1 — getResourceForOrg no-namespace denial', () => {
445
+ test('getResourceForOrg when resource has no namespace throws cross-org error', async () => {
446
+ const noNsResource = {
447
+ resource: createResource('Repository', { name: 'no-ns-get-repo' }, {
448
+ organizationRef: 'org-a',
449
+ visibility: 'internal'
450
+ })
451
+ };
452
+ delete noNsResource.resource.metadata.namespace;
453
+
454
+ const { controller } = await makeController({ getResult: noNsResource });
455
+
456
+ await assert.rejects(
457
+ () => controller.getResourceForOrg('org-a', 'Repository', 'no-ns-get-repo'),
458
+ (err) => {
459
+ assert.match(err.message, /cross.org|does not match/i,
460
+ 'error must describe cross-org denial for absent namespace');
461
+ return true;
462
+ }
463
+ );
464
+ });
465
+ });
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // Test 17 — applyResource with organizationRef but no namespace auto-assigns
469
+ // ---------------------------------------------------------------------------
470
+ describe('Slice 1.1 — applyResource auto-assigns namespace from organizationRef', () => {
471
+ test('applyResource with organizationRef but no namespace auto-assigns namespace', async () => {
472
+ const { controller, calls } = await makeController();
473
+
474
+ const resource = {
475
+ apiVersion: 'krate.a5c.ai/v1alpha1',
476
+ kind: 'Repository',
477
+ metadata: {
478
+ name: 'auto-ns-repo',
479
+ labels: {}
480
+ },
481
+ spec: {
482
+ organizationRef: 'org-a',
483
+ visibility: 'internal'
484
+ }
485
+ };
486
+
487
+ const result = await controller.applyResource(resource);
488
+
489
+ assert.ok(result, 'apply must succeed');
490
+ assert.equal(calls.applied.length, 1);
491
+ assert.equal(calls.applied[0].metadata.namespace, 'krate-org-org-a',
492
+ 'namespace must be auto-assigned from organizationRef');
493
+ });
494
+ });
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // Test 18 — deleteResourceForOrg when resource not found returns gracefully
498
+ // ---------------------------------------------------------------------------
499
+ describe('Slice 1.1 — deleteResourceForOrg not found', () => {
500
+ test('deleteResourceForOrg when resource not found returns gracefully', async () => {
501
+ // getResult = null means resource not found
502
+ const { controller, calls } = await makeController({ getResult: null });
503
+
504
+ const result = await controller.deleteResourceForOrg('org-a', 'Repository', 'nonexistent-repo');
505
+
506
+ assert.ok(result, 'delete must return a result even when resource is not found');
507
+ assert.equal(calls.deleted.length, 1, 'gateway.delete must still be called');
508
+ assert.equal(calls.deleted[0].name, 'nonexistent-repo');
509
+ });
510
+ });
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Test 19 — normalizeOrgSlug collision detection note
514
+ // ---------------------------------------------------------------------------
515
+ describe('Slice 1.1 — normalizeOrgSlug collision detection', () => {
516
+ test('normalizeOrgSlug collision: org-a-b and org_a_b normalize identically', () => {
517
+ // This test documents that underscores and hyphens collapse to the same slug.
518
+ // Real production code should add collision detection when creating organizations.
519
+ assert.equal(normalizeOrgSlug('org-a-b'), normalizeOrgSlug('org_a_b'),
520
+ 'org-a-b and org_a_b must normalize to the same slug (collision)');
521
+ assert.equal(normalizeOrgSlug('org_a_b'), 'org-a-b',
522
+ 'underscores must be replaced by hyphens in normalized form');
523
+ });
524
+ });
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // Test 20 — normalizeOrgSlug edge cases
528
+ // ---------------------------------------------------------------------------
529
+ describe('Slice 1.1 — normalizeOrgSlug edge cases', () => {
530
+ test('normalizeOrgSlug handles empty string', () => {
531
+ assert.equal(normalizeOrgSlug(''), '');
532
+ });
533
+
534
+ test('normalizeOrgSlug handles null/undefined', () => {
535
+ assert.equal(normalizeOrgSlug(null), '');
536
+ assert.equal(normalizeOrgSlug(undefined), '');
537
+ });
538
+
539
+ test('normalizeOrgSlug lowercases and strips special chars', () => {
540
+ assert.equal(normalizeOrgSlug('My_Org!@#'), 'my-org');
541
+ });
542
+
543
+ test('normalizeOrgSlug strips leading and trailing hyphens', () => {
544
+ assert.equal(normalizeOrgSlug('--my-org--'), 'my-org');
545
+ });
546
+
547
+ test('normalizeOrgSlug truncates to 63 chars', () => {
548
+ const longSlug = 'a'.repeat(100);
549
+ assert.ok(normalizeOrgSlug(longSlug).length <= 63);
550
+ });
551
+ });
552
+
553
+ // ---------------------------------------------------------------------------
554
+ // Test 15 — listResourceForOrg with empty results
555
+ // ---------------------------------------------------------------------------
556
+ describe('Slice 1.1 — listResourceForOrg empty results', () => {
557
+ test('listResourceForOrg returns empty items when no resources match', async () => {
558
+ const repoOrgB = createResource(
559
+ 'Repository',
560
+ { name: 'repo-beta', namespace: 'krate-org-org-b' },
561
+ { organizationRef: 'org-b', visibility: 'private' }
562
+ );
563
+
564
+ const { controller } = await makeController({ listResult: { items: [repoOrgB] } });
565
+
566
+ const result = await controller.listResourceForOrg('org-a', 'Repository');
567
+
568
+ assert.ok(Array.isArray(result.items), 'result must have an items array');
569
+ assert.equal(result.items.length, 0, 'no items should match org-a');
570
+ });
571
+ });
572
+
573
+ // ---------------------------------------------------------------------------
574
+ // Test 16 — listResourceForOrg multi-org filtering
575
+ // ---------------------------------------------------------------------------
576
+ describe('Slice 1.1 — listResourceForOrg multi-org filtering', () => {
577
+ test('listResourceForOrg correctly filters across multiple orgs', async () => {
578
+ const repoA1 = createResource('Repository', { name: 'a-repo-1', namespace: 'krate-org-org-a' }, { organizationRef: 'org-a', visibility: 'internal' });
579
+ const repoA2 = createResource('Repository', { name: 'a-repo-2', namespace: 'krate-org-org-a' }, { organizationRef: 'org-a', visibility: 'internal' });
580
+ const repoB1 = createResource('Repository', { name: 'b-repo-1', namespace: 'krate-org-org-b' }, { organizationRef: 'org-b', visibility: 'private' });
581
+ const repoC1 = createResource('Repository', { name: 'c-repo-1', namespace: 'krate-org-org-c' }, { organizationRef: 'org-c', visibility: 'public' });
582
+
583
+ const { controller } = await makeController({ listResult: { items: [repoA1, repoA2, repoB1, repoC1] } });
584
+
585
+ const resultA = await controller.listResourceForOrg('org-a', 'Repository');
586
+ assert.equal(resultA.items.length, 2, 'org-a should have 2 repos');
587
+ assert.deepEqual(resultA.items.map((i) => i.metadata.name).sort(), ['a-repo-1', 'a-repo-2']);
588
+
589
+ const resultB = await controller.listResourceForOrg('org-b', 'Repository');
590
+ assert.equal(resultB.items.length, 1, 'org-b should have 1 repo');
591
+ assert.equal(resultB.items[0].metadata.name, 'b-repo-1');
592
+
593
+ const resultC = await controller.listResourceForOrg('org-c', 'Repository');
594
+ assert.equal(resultC.items.length, 1, 'org-c should have 1 repo');
595
+ });
596
+ });
597
+
598
+ // ---------------------------------------------------------------------------
599
+ // Test 17 — deleteResource emits audit event
600
+ // ---------------------------------------------------------------------------
601
+ describe('Slice 1.1 — deleteResource audit', () => {
602
+ test('deleteResource emits an audit event', async () => {
603
+ const auditEvents = [];
604
+
605
+ const { controller } = await makeController({}, {
606
+ onAuditEvent: (event) => auditEvents.push(event)
607
+ });
608
+
609
+ await controller.deleteResource('Repository', 'some-repo');
610
+
611
+ assert.ok(auditEvents.length >= 1, 'at least one audit event must be emitted');
612
+ const event = auditEvents.find((e) => e.operation === 'delete');
613
+ assert.ok(event, 'audit event with operation "delete" must exist');
614
+ assert.equal(event.name, 'some-repo');
615
+ assert.ok(event.timestamp, 'event.timestamp must be present');
616
+ });
617
+ });
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // Test 18 — Cross-org denial for system namespaces
621
+ // (Regression: resources targeting system namespaces like krate-system
622
+ // with any organizationRef must also be blocked.)
623
+ // ---------------------------------------------------------------------------
624
+ describe('Slice 1.1 — Cross-org denial for system namespaces', () => {
625
+ test('applyResource rejects resource targeting krate-system with an organizationRef', async () => {
626
+ const { controller, calls } = await makeController();
627
+
628
+ const systemNsResource = {
629
+ apiVersion: 'krate.a5c.ai/v1alpha1',
630
+ kind: 'Repository',
631
+ metadata: {
632
+ name: 'sneaky-repo',
633
+ namespace: 'krate-system',
634
+ labels: {}
635
+ },
636
+ spec: {
637
+ organizationRef: 'org-a',
638
+ visibility: 'internal'
639
+ }
640
+ };
641
+
642
+ await assert.rejects(
643
+ () => controller.applyResource(systemNsResource),
644
+ (err) => {
645
+ assert.match(
646
+ err.message,
647
+ /cross.org|org.*mismatch|namespace.*does not match/i,
648
+ 'error message must describe the namespace/org mismatch'
649
+ );
650
+ return true;
651
+ }
652
+ );
653
+
654
+ assert.equal(calls.applied.length, 0, 'gateway.apply must not be called');
655
+ });
656
+
657
+ test('applyResource rejects resource targeting default namespace with an organizationRef', async () => {
658
+ const { controller, calls } = await makeController();
659
+
660
+ const defaultNsResource = {
661
+ apiVersion: 'krate.a5c.ai/v1alpha1',
662
+ kind: 'Repository',
663
+ metadata: {
664
+ name: 'sneaky-repo-2',
665
+ namespace: 'default',
666
+ labels: {}
667
+ },
668
+ spec: {
669
+ organizationRef: 'org-a',
670
+ visibility: 'internal'
671
+ }
672
+ };
673
+
674
+ await assert.rejects(
675
+ () => controller.applyResource(defaultNsResource),
676
+ (err) => {
677
+ assert.match(
678
+ err.message,
679
+ /cross.org|org.*mismatch|namespace.*does not match/i
680
+ );
681
+ return true;
682
+ }
683
+ );
684
+
685
+ assert.equal(calls.applied.length, 0);
686
+ });
687
+ });