@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,198 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentTriggerController, createResource } from '../src/index.js';
4
+
5
+ // ─── helpers ─────────────────────────────────────────────────────────────────
6
+
7
+ function makeWebhookRule(name, webhookTrigger = {}, extra = {}) {
8
+ return createResource('AgentTriggerRule', { name, namespace: 'krate-org-default' }, {
9
+ organizationRef: 'default',
10
+ agentStack: 'ci-stack',
11
+ taskKind: 'diagnostic',
12
+ webhookTrigger,
13
+ ...extra,
14
+ });
15
+ }
16
+
17
+ function makeWebhookEvent(overrides = {}) {
18
+ return {
19
+ eventType: 'push',
20
+ repository: 'owner/repo',
21
+ ref: 'refs/heads/main',
22
+ action: null,
23
+ provider: 'github',
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ // ─── tests ───────────────────────────────────────────────────────────────────
29
+
30
+ // 1. evaluateWebhookEvent matches push event to a push trigger rule
31
+ test('evaluateWebhookEvent: matches push event to push trigger rule', () => {
32
+ const ctrl = createAgentTriggerController();
33
+ const rule = makeWebhookRule('push-rule', { events: ['push'] });
34
+ const event = makeWebhookEvent({ eventType: 'push' });
35
+
36
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
37
+
38
+ assert.equal(matchingRules.length, 1);
39
+ assert.equal(matchingRules[0].metadata.name, 'push-rule');
40
+ assert.equal(dispatchIntents.length, 1);
41
+ assert.equal(dispatchIntents[0].agentStack, 'ci-stack');
42
+ assert.equal(dispatchIntents[0].taskKind, 'diagnostic');
43
+ });
44
+
45
+ // 2. evaluateWebhookEvent matches pull_request event to pull_request trigger rule
46
+ test('evaluateWebhookEvent: matches pull_request event to pull_request trigger rule', () => {
47
+ const ctrl = createAgentTriggerController();
48
+ const rule = makeWebhookRule('pr-rule', { events: ['pull_request'] });
49
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'opened' });
50
+
51
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
52
+
53
+ assert.equal(matchingRules.length, 1);
54
+ assert.equal(matchingRules[0].metadata.name, 'pr-rule');
55
+ assert.equal(dispatchIntents[0].taskKind, 'diagnostic');
56
+ });
57
+
58
+ // 3. evaluateWebhookEvent returns empty when no rules match event type
59
+ test('evaluateWebhookEvent: returns empty when no rules match event type', () => {
60
+ const ctrl = createAgentTriggerController();
61
+ const rule = makeWebhookRule('push-only', { events: ['push'] });
62
+ const event = makeWebhookEvent({ eventType: 'issues' });
63
+
64
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
65
+
66
+ assert.equal(matchingRules.length, 0);
67
+ assert.equal(dispatchIntents.length, 0);
68
+ });
69
+
70
+ // 4. evaluateWebhookEvent filters by repository — matches when equal
71
+ test('evaluateWebhookEvent: filters by repository — matches when equal', () => {
72
+ const ctrl = createAgentTriggerController();
73
+ const rule = makeWebhookRule('repo-rule', { events: ['push'], repository: 'owner/repo' });
74
+ const event = makeWebhookEvent({ eventType: 'push', repository: 'owner/repo' });
75
+
76
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
77
+
78
+ assert.equal(matchingRules.length, 1);
79
+ });
80
+
81
+ // 5. evaluateWebhookEvent filters by repository — no match when different
82
+ test('evaluateWebhookEvent: filters by repository — no match when different', () => {
83
+ const ctrl = createAgentTriggerController();
84
+ const rule = makeWebhookRule('repo-rule', { events: ['push'], repository: 'owner/repo' });
85
+ const event = makeWebhookEvent({ eventType: 'push', repository: 'other/repo' });
86
+
87
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
88
+
89
+ assert.equal(matchingRules.length, 0);
90
+ });
91
+
92
+ // 6. evaluateWebhookEvent respects enabled: false — disabled rules are excluded
93
+ test('evaluateWebhookEvent: respects enabled: false — disabled rules are excluded', () => {
94
+ const ctrl = createAgentTriggerController();
95
+ const rule = makeWebhookRule('disabled-rule', { events: ['push'] }, { enabled: false });
96
+ const event = makeWebhookEvent({ eventType: 'push' });
97
+
98
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
99
+
100
+ assert.equal(matchingRules.length, 0);
101
+ });
102
+
103
+ // 7. evaluateWebhookEvent deduplicates rules with the same name
104
+ test('evaluateWebhookEvent: deduplicates rules with the same name', () => {
105
+ const ctrl = createAgentTriggerController();
106
+ const rule1 = makeWebhookRule('dup-rule', { events: ['push'] });
107
+ const rule2 = makeWebhookRule('dup-rule', { events: ['push'] }); // same name
108
+ const event = makeWebhookEvent({ eventType: 'push' });
109
+
110
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule1, rule2]);
111
+
112
+ assert.equal(matchingRules.length, 1);
113
+ });
114
+
115
+ // 8. evaluateWebhookEvent handles empty rules array
116
+ test('evaluateWebhookEvent: handles empty rules array', () => {
117
+ const ctrl = createAgentTriggerController();
118
+ const event = makeWebhookEvent();
119
+
120
+ const { matchingRules, dispatchIntents } = ctrl.evaluateWebhookEvent(event, []);
121
+
122
+ assert.equal(matchingRules.length, 0);
123
+ assert.equal(dispatchIntents.length, 0);
124
+ });
125
+
126
+ // 9. evaluateWebhookEvent handles undefined/null rules gracefully
127
+ test('evaluateWebhookEvent: handles undefined rules gracefully', () => {
128
+ const ctrl = createAgentTriggerController();
129
+ const event = makeWebhookEvent();
130
+
131
+ const resultUndefined = ctrl.evaluateWebhookEvent(event, undefined);
132
+ assert.equal(resultUndefined.matchingRules.length, 0);
133
+ assert.equal(resultUndefined.dispatchIntents.length, 0);
134
+
135
+ const resultNull = ctrl.evaluateWebhookEvent(event, null);
136
+ assert.equal(resultNull.matchingRules.length, 0);
137
+ assert.equal(resultNull.dispatchIntents.length, 0);
138
+ });
139
+
140
+ // 10. evaluateWebhookEvent matches wildcard events array ['*']
141
+ test('evaluateWebhookEvent: matches wildcard events array [\'*\']', () => {
142
+ const ctrl = createAgentTriggerController();
143
+ const rule = makeWebhookRule('wildcard-rule', { events: ['*'] });
144
+ const events = ['push', 'pull_request', 'issues', 'workflow_run', 'ping'];
145
+
146
+ for (const eventType of events) {
147
+ const { matchingRules } = ctrl.evaluateWebhookEvent(makeWebhookEvent({ eventType }), [rule]);
148
+ assert.equal(matchingRules.length, 1, `Should match event type '${eventType}' with wildcard`);
149
+ }
150
+ });
151
+
152
+ // 11. evaluateWebhookEvent filters by action — matches when equal
153
+ test('evaluateWebhookEvent: filters by action — matches when equal', () => {
154
+ const ctrl = createAgentTriggerController();
155
+ const rule = makeWebhookRule('pr-opened-rule', { events: ['pull_request'], action: 'opened' });
156
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'opened' });
157
+
158
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
159
+
160
+ assert.equal(matchingRules.length, 1);
161
+ });
162
+
163
+ // 12. evaluateWebhookEvent filters by action — no match when action differs
164
+ test('evaluateWebhookEvent: filters by action — no match when action differs', () => {
165
+ const ctrl = createAgentTriggerController();
166
+ const rule = makeWebhookRule('pr-opened-rule', { events: ['pull_request'], action: 'opened' });
167
+ const event = makeWebhookEvent({ eventType: 'pull_request', action: 'closed' });
168
+
169
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
170
+
171
+ assert.equal(matchingRules.length, 0);
172
+ });
173
+
174
+ // 13. evaluateWebhookEvent matches when events array is absent (match all)
175
+ test('evaluateWebhookEvent: matches when events array is absent (match all)', () => {
176
+ const ctrl = createAgentTriggerController();
177
+ // webhookTrigger present but no events filter
178
+ const rule = makeWebhookRule('no-events-filter', {});
179
+ const event = makeWebhookEvent({ eventType: 'workflow_run' });
180
+
181
+ const { matchingRules } = ctrl.evaluateWebhookEvent(event, [rule]);
182
+
183
+ assert.equal(matchingRules.length, 1);
184
+ });
185
+
186
+ // 14. dispatchIntent includes correct agentStack and uses custom taskKind
187
+ test('evaluateWebhookEvent: dispatchIntent includes agentStack and custom taskKind', () => {
188
+ const ctrl = createAgentTriggerController();
189
+ const rule = makeWebhookRule('custom-kind-rule', { events: ['push'] }, { taskKind: 'repair', agentStack: 'my-stack' });
190
+ const event = makeWebhookEvent({ eventType: 'push' });
191
+
192
+ const { dispatchIntents } = ctrl.evaluateWebhookEvent(event, [rule]);
193
+
194
+ assert.equal(dispatchIntents.length, 1);
195
+ assert.equal(dispatchIntents[0].agentStack, 'my-stack');
196
+ assert.equal(dispatchIntents[0].taskKind, 'repair');
197
+ assert.deepEqual(dispatchIntents[0].event, event);
198
+ });
@@ -0,0 +1,312 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentWorkspaceController, AGENT_WORKSPACE_CONTROLLER_BOUNDARY, createResource } from '../src/index.js';
4
+
5
+ function makeWorkspace(name, opts = {}) {
6
+ const ws = createResource('KrateWorkspace', { name, namespace: opts.namespace || 'default' }, {
7
+ organizationRef: opts.organizationRef || 'default',
8
+ repository: opts.repository || 'https://github.com/acme/app.git',
9
+ volumeSpec: opts.volumeSpec || { storageClassName: 'standard', capacity: '10Gi', accessModes: ['ReadWriteOnce'] },
10
+ branch: opts.branch || 'main',
11
+ pvcName: opts.pvcName || `krate-ws-${name}`,
12
+ });
13
+ ws.status = {
14
+ phase: opts.phase || 'Ready',
15
+ volumeStatus: opts.volumeStatus || 'Bound',
16
+ createdAt: new Date().toISOString(),
17
+ ...(opts.status || {}),
18
+ };
19
+ return ws;
20
+ }
21
+
22
+ // --- createWorkspace ---
23
+
24
+ test('createWorkspace generates valid PVC manifest with correct labels and capacity', () => {
25
+ const ctrl = createAgentWorkspaceController();
26
+ const result = ctrl.createWorkspace({
27
+ name: 'my-ws',
28
+ organizationRef: 'acme',
29
+ repository: 'https://github.com/acme/app.git',
30
+ volumeSpec: { capacity: '20Gi' },
31
+ });
32
+
33
+ assert.equal(result.error, false);
34
+ assert.ok(result.workspace);
35
+ assert.ok(result.pvcManifest);
36
+ assert.equal(result.pvcManifest.kind, 'PersistentVolumeClaim');
37
+ assert.equal(result.pvcManifest.metadata.name, 'krate-ws-my-ws');
38
+ assert.equal(result.pvcManifest.metadata.labels['krate.a5c.ai/workspace'], 'my-ws');
39
+ assert.equal(result.pvcManifest.metadata.labels['krate.a5c.ai/org'], 'acme');
40
+ assert.equal(result.pvcManifest.spec.resources.requests.storage, '20Gi');
41
+ });
42
+
43
+ test('createWorkspace uses custom storageClassName when provided', () => {
44
+ const ctrl = createAgentWorkspaceController();
45
+ const result = ctrl.createWorkspace({
46
+ name: 'custom-sc',
47
+ organizationRef: 'acme',
48
+ repository: 'https://github.com/acme/app.git',
49
+ volumeSpec: { storageClassName: 'fast-ssd' },
50
+ });
51
+
52
+ assert.equal(result.error, false);
53
+ assert.equal(result.pvcManifest.spec.storageClassName, 'fast-ssd');
54
+ assert.equal(result.workspace.spec.volumeSpec.storageClassName, 'fast-ssd');
55
+ });
56
+
57
+ test('createWorkspace defaults to 10Gi capacity', () => {
58
+ const ctrl = createAgentWorkspaceController();
59
+ const result = ctrl.createWorkspace({
60
+ name: 'default-cap',
61
+ organizationRef: 'acme',
62
+ repository: 'https://github.com/acme/app.git',
63
+ });
64
+
65
+ assert.equal(result.error, false);
66
+ assert.equal(result.pvcManifest.spec.resources.requests.storage, '10Gi');
67
+ assert.equal(result.workspace.spec.volumeSpec.capacity, '10Gi');
68
+ });
69
+
70
+ test('createWorkspace validates required fields (org, repository)', () => {
71
+ const ctrl = createAgentWorkspaceController();
72
+
73
+ const noOrg = ctrl.createWorkspace({ name: 'x', repository: 'repo' });
74
+ assert.equal(noOrg.error, true);
75
+ assert.equal(noOrg.reason, 'missing-org');
76
+
77
+ const noRepo = ctrl.createWorkspace({ name: 'x', organizationRef: 'acme' });
78
+ assert.equal(noRepo.error, true);
79
+ assert.equal(noRepo.reason, 'missing-repository');
80
+ });
81
+
82
+ test('createWorkspace rejects null resource (missing both org and repo)', () => {
83
+ const ctrl = createAgentWorkspaceController();
84
+ const result = ctrl.createWorkspace({});
85
+ assert.equal(result.error, true);
86
+ });
87
+
88
+ // --- deleteWorkspace ---
89
+
90
+ test('deleteWorkspace generates PVC delete manifest', () => {
91
+ const ctrl = createAgentWorkspaceController();
92
+ const ws = makeWorkspace('del-ws', { phase: 'Ready' });
93
+
94
+ const result = ctrl.deleteWorkspace({ name: 'del-ws', resources: { KrateWorkspace: [ws] } });
95
+
96
+ assert.equal(result.error, false);
97
+ assert.equal(result.workspace.status.phase, 'Terminating');
98
+ assert.ok(result.pvcDeleteManifest);
99
+ assert.equal(result.pvcDeleteManifest.metadata.name, 'krate-ws-del-ws');
100
+ assert.equal(result.pvcDeleteManifest.action, 'delete');
101
+ });
102
+
103
+ // --- getWorkspaceStatus ---
104
+
105
+ test('getWorkspaceStatus returns Pending for new workspace', () => {
106
+ const ctrl = createAgentWorkspaceController();
107
+ const ws = makeWorkspace('new-ws', { phase: 'Pending', volumeStatus: 'Pending' });
108
+
109
+ const result = ctrl.getWorkspaceStatus({ name: 'new-ws', resources: { KrateWorkspace: [ws] } });
110
+
111
+ assert.equal(result.error, false);
112
+ assert.equal(result.volumeStatus, 'Pending');
113
+ assert.equal(result.phase, 'Pending');
114
+ });
115
+
116
+ // --- initializeWorkspace ---
117
+
118
+ test('initializeWorkspace returns git clone command for https repo', () => {
119
+ const ctrl = createAgentWorkspaceController();
120
+ const ws = makeWorkspace('https-ws', { repository: 'https://github.com/acme/app.git' });
121
+
122
+ const result = ctrl.initializeWorkspace({ workspace: ws });
123
+
124
+ assert.equal(result.error, false);
125
+ assert.equal(result.commandSpec.command, 'git');
126
+ assert.deepEqual(result.commandSpec.args, ['clone', 'https://github.com/acme/app.git', '/workspace']);
127
+ assert.equal(Object.keys(result.commandSpec.env).length, 0, 'No special env for https');
128
+ });
129
+
130
+ test('initializeWorkspace returns git clone command for ssh repo', () => {
131
+ const ctrl = createAgentWorkspaceController();
132
+ const ws = makeWorkspace('ssh-ws', { repository: 'git@github.com:acme/app.git' });
133
+
134
+ const result = ctrl.initializeWorkspace({ workspace: ws });
135
+
136
+ assert.equal(result.error, false);
137
+ assert.equal(result.commandSpec.command, 'git');
138
+ assert.deepEqual(result.commandSpec.args, ['clone', 'git@github.com:acme/app.git', '/workspace']);
139
+ assert.ok(result.commandSpec.env.GIT_SSH_COMMAND, 'Should set GIT_SSH_COMMAND for SSH repos');
140
+ });
141
+
142
+ // --- checkoutBranch ---
143
+
144
+ test('checkoutBranch returns git checkout command', () => {
145
+ const ctrl = createAgentWorkspaceController();
146
+ const ws = makeWorkspace('co-ws');
147
+
148
+ const result = ctrl.checkoutBranch({ workspace: ws, branch: 'feature-42' });
149
+
150
+ assert.equal(result.error, false);
151
+ assert.equal(result.commandSpec.command, 'git');
152
+ assert.deepEqual(result.commandSpec.args, ['checkout', 'feature-42']);
153
+ assert.equal(result.commandSpec.cwd, '/workspace');
154
+ });
155
+
156
+ // --- syncWorkspace ---
157
+
158
+ test('syncWorkspace returns git fetch + reset commands', () => {
159
+ const ctrl = createAgentWorkspaceController();
160
+ const ws = makeWorkspace('sync-ws', { branch: 'develop' });
161
+
162
+ const result = ctrl.syncWorkspace({ workspace: ws });
163
+
164
+ assert.equal(result.error, false);
165
+ assert.equal(result.commandSpecs.length, 2);
166
+ assert.deepEqual(result.commandSpecs[0].args, ['fetch', 'origin']);
167
+ assert.deepEqual(result.commandSpecs[1].args, ['reset', '--hard', 'origin/develop']);
168
+ });
169
+
170
+ // --- getMountSpec ---
171
+
172
+ test('getMountSpec returns valid K8s volume and volumeMount', () => {
173
+ const ctrl = createAgentWorkspaceController();
174
+ const ws = makeWorkspace('mount-ws', { pvcName: 'krate-ws-mount-ws' });
175
+
176
+ const result = ctrl.getMountSpec({ workspace: ws });
177
+
178
+ assert.equal(result.error, false);
179
+ assert.ok(result.volume);
180
+ assert.ok(result.volumeMount);
181
+ assert.equal(result.volume.name, 'workspace');
182
+ assert.equal(result.volumeMount.name, 'workspace');
183
+ assert.equal(result.volumeMount.mountPath, '/workspace');
184
+ });
185
+
186
+ test('getMountSpec uses correct PVC claim name', () => {
187
+ const ctrl = createAgentWorkspaceController();
188
+ const ws = makeWorkspace('pvc-test', { pvcName: 'krate-ws-pvc-test' });
189
+
190
+ const result = ctrl.getMountSpec({ workspace: ws });
191
+
192
+ assert.equal(result.error, false);
193
+ assert.equal(result.volume.persistentVolumeClaim.claimName, 'krate-ws-pvc-test');
194
+ });
195
+
196
+ // --- findReusableWorkspace ---
197
+
198
+ test('findReusableWorkspace returns matching Ready workspace', () => {
199
+ const ctrl = createAgentWorkspaceController();
200
+ const ws1 = makeWorkspace('reuse-ws', {
201
+ organizationRef: 'acme',
202
+ repository: 'https://github.com/acme/app.git',
203
+ branch: 'main',
204
+ phase: 'Ready',
205
+ });
206
+ const ws2 = makeWorkspace('other-ws', {
207
+ organizationRef: 'acme',
208
+ repository: 'https://github.com/acme/other.git',
209
+ branch: 'main',
210
+ phase: 'Ready',
211
+ });
212
+
213
+ const result = ctrl.findReusableWorkspace({
214
+ organizationRef: 'acme',
215
+ repository: 'https://github.com/acme/app.git',
216
+ branch: 'main',
217
+ resources: { KrateWorkspace: [ws1, ws2] },
218
+ });
219
+
220
+ assert.ok(result);
221
+ assert.equal(result.metadata.name, 'reuse-ws');
222
+ });
223
+
224
+ test('findReusableWorkspace returns null when no match', () => {
225
+ const ctrl = createAgentWorkspaceController();
226
+ const ws = makeWorkspace('busy-ws', {
227
+ organizationRef: 'acme',
228
+ repository: 'https://github.com/acme/app.git',
229
+ branch: 'main',
230
+ phase: 'InUse',
231
+ });
232
+
233
+ const result = ctrl.findReusableWorkspace({
234
+ organizationRef: 'acme',
235
+ repository: 'https://github.com/acme/app.git',
236
+ branch: 'main',
237
+ resources: { KrateWorkspace: [ws] },
238
+ });
239
+
240
+ assert.equal(result, null);
241
+ });
242
+
243
+ // --- claimWorkspace ---
244
+
245
+ test('claimWorkspace transitions to InUse with runRef', () => {
246
+ const ctrl = createAgentWorkspaceController();
247
+ const ws = makeWorkspace('claim-ws', { phase: 'Ready' });
248
+
249
+ const result = ctrl.claimWorkspace({
250
+ name: 'claim-ws',
251
+ runRef: 'run-123',
252
+ resources: { KrateWorkspace: [ws] },
253
+ });
254
+
255
+ assert.equal(result.error, false);
256
+ assert.equal(result.workspace.status.phase, 'InUse');
257
+ assert.equal(result.workspace.status.runRef, 'run-123');
258
+ assert.ok(result.workspace.status.claimedAt);
259
+ });
260
+
261
+ test('claimWorkspace rejects already-InUse workspace', () => {
262
+ const ctrl = createAgentWorkspaceController();
263
+ const ws = makeWorkspace('busy-ws', { phase: 'InUse', status: { runRef: 'run-existing' } });
264
+
265
+ const result = ctrl.claimWorkspace({
266
+ name: 'busy-ws',
267
+ runRef: 'run-new',
268
+ resources: { KrateWorkspace: [ws] },
269
+ });
270
+
271
+ assert.equal(result.error, true);
272
+ assert.equal(result.reason, 'already-in-use');
273
+ });
274
+
275
+ // --- releaseWorkspace ---
276
+
277
+ test('releaseWorkspace transitions to Ready', () => {
278
+ const ctrl = createAgentWorkspaceController();
279
+ const ws = makeWorkspace('release-ws', { phase: 'InUse', status: { runRef: 'run-done' } });
280
+
281
+ const result = ctrl.releaseWorkspace({
282
+ name: 'release-ws',
283
+ resources: { KrateWorkspace: [ws] },
284
+ });
285
+
286
+ assert.equal(result.error, false);
287
+ assert.equal(result.workspace.status.phase, 'Ready');
288
+ assert.equal(result.workspace.status.runRef, undefined);
289
+ assert.ok(result.workspace.status.releasedAt);
290
+ });
291
+
292
+ test('releaseWorkspace rejects non-InUse workspace', () => {
293
+ const ctrl = createAgentWorkspaceController();
294
+ const ws = makeWorkspace('ready-ws', { phase: 'Ready' });
295
+
296
+ const result = ctrl.releaseWorkspace({
297
+ name: 'ready-ws',
298
+ resources: { KrateWorkspace: [ws] },
299
+ });
300
+
301
+ assert.equal(result.error, true);
302
+ assert.equal(result.reason, 'not-in-use');
303
+ });
304
+
305
+ // --- BOUNDARY export ---
306
+
307
+ test('BOUNDARY exported', () => {
308
+ assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY);
309
+ assert.equal(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.role, 'agent-workspace-controller');
310
+ assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.owns.includes('PVC manifest generation'));
311
+ assert.ok(AGENT_WORKSPACE_CONTROLLER_BOUNDARY.owns.includes('workspace reuse'));
312
+ });