@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,190 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { normalizeWebhookEvent } from '../src/index.js';
4
+
5
+ describe('normalizeWebhookEvent', () => {
6
+ // 1. GitHub workflow_run failure -> type: 'ci-failure'
7
+ it('normalizes GitHub workflow_run failure to ci-failure', () => {
8
+ const body = {
9
+ action: 'completed',
10
+ workflow_run: { conclusion: 'failure', name: 'CI Build', head_branch: 'feature-x' },
11
+ repository: { full_name: 'acme/app' },
12
+ sender: { login: 'alice' }
13
+ };
14
+ const event = normalizeWebhookEvent(body, 'acme');
15
+
16
+ assert.equal(event.type, 'ci-failure');
17
+ assert.equal(event.source.kind, 'Pipeline');
18
+ assert.equal(event.source.name, 'CI Build');
19
+ assert.equal(event.repository, 'acme/app');
20
+ assert.equal(event.ref, 'feature-x');
21
+ assert.equal(event.actor, 'alice');
22
+ assert.equal(event.payload, body);
23
+ });
24
+
25
+ // 2. GitHub PR opened -> type: 'pr-opened'
26
+ it('normalizes GitHub PR opened to pr-opened', () => {
27
+ const body = {
28
+ action: 'opened',
29
+ pull_request: { number: 42, head: { ref: 'feat/login' } },
30
+ repository: { full_name: 'acme/app' },
31
+ sender: { login: 'bob' }
32
+ };
33
+ const event = normalizeWebhookEvent(body, 'acme');
34
+
35
+ assert.equal(event.type, 'pr-opened');
36
+ assert.equal(event.source.kind, 'PullRequest');
37
+ assert.equal(event.source.name, '42');
38
+ assert.equal(event.repository, 'acme/app');
39
+ assert.equal(event.ref, 'feat/login');
40
+ assert.equal(event.actor, 'bob');
41
+ assert.equal(event.payload, body);
42
+ });
43
+
44
+ // 3. GitHub issue comment -> type: 'comment' with kind 'Issue'
45
+ it('normalizes GitHub issue comment to comment with kind Issue', () => {
46
+ const body = {
47
+ action: 'created',
48
+ comment: { body: 'Looks good!', user: { login: 'reviewer' } },
49
+ issue: { number: 10 },
50
+ repository: { full_name: 'acme/app' },
51
+ sender: { login: 'reviewer' }
52
+ };
53
+ const event = normalizeWebhookEvent(body, 'acme');
54
+
55
+ assert.equal(event.type, 'comment');
56
+ assert.equal(event.source.kind, 'Issue');
57
+ assert.equal(event.source.name, '10');
58
+ assert.equal(event.repository, 'acme/app');
59
+ assert.equal(event.actor, 'reviewer');
60
+ assert.deepEqual(event.payload, { body: 'Looks good!' });
61
+ });
62
+
63
+ // 4. GitHub PR comment -> type: 'comment' with kind 'PullRequest'
64
+ it('normalizes GitHub PR comment to comment with kind PullRequest', () => {
65
+ const body = {
66
+ action: 'created',
67
+ comment: { body: 'LGTM', user: { login: 'reviewer' } },
68
+ issue: { number: 7, pull_request: { url: 'https://api.github.com/repos/acme/app/pulls/7' } },
69
+ repository: { full_name: 'acme/app' },
70
+ sender: { login: 'reviewer' }
71
+ };
72
+ const event = normalizeWebhookEvent(body, 'acme');
73
+
74
+ assert.equal(event.type, 'comment');
75
+ assert.equal(event.source.kind, 'PullRequest');
76
+ assert.equal(event.source.name, '7');
77
+ assert.equal(event.repository, 'acme/app');
78
+ assert.equal(event.actor, 'reviewer');
79
+ assert.deepEqual(event.payload, { body: 'LGTM' });
80
+ });
81
+
82
+ // 5. GitHub label added -> type: 'label-added'
83
+ it('normalizes GitHub label added to label-added', () => {
84
+ const body = {
85
+ action: 'labeled',
86
+ label: { name: 'bug' },
87
+ issue: { number: 15 },
88
+ repository: { full_name: 'acme/app' },
89
+ sender: { login: 'triager' }
90
+ };
91
+ const event = normalizeWebhookEvent(body, 'acme');
92
+
93
+ assert.equal(event.type, 'label-added');
94
+ assert.equal(event.source.kind, 'Issue');
95
+ assert.equal(event.source.name, '15');
96
+ assert.equal(event.repository, 'acme/app');
97
+ assert.equal(event.actor, 'triager');
98
+ assert.deepEqual(event.payload, { label: 'bug' });
99
+ });
100
+
101
+ // 6. GitHub push -> type: 'push'
102
+ it('normalizes GitHub push to push', () => {
103
+ const body = {
104
+ ref: 'refs/heads/main',
105
+ commits: [{ id: 'abc123', message: 'fix typo' }],
106
+ repository: { full_name: 'acme/app' },
107
+ sender: { login: 'dev' },
108
+ pusher: { name: 'dev' }
109
+ };
110
+ const event = normalizeWebhookEvent(body, 'acme');
111
+
112
+ assert.equal(event.type, 'push');
113
+ assert.equal(event.source.kind, 'Repository');
114
+ assert.equal(event.source.name, 'acme/app');
115
+ assert.equal(event.repository, 'acme/app');
116
+ assert.equal(event.ref, 'main');
117
+ assert.equal(event.actor, 'dev');
118
+ assert.equal(event.payload, body);
119
+ });
120
+
121
+ // 7. Unknown payload -> type: 'webhook' (fallback)
122
+ it('returns webhook fallback for unknown payload', () => {
123
+ const body = {
124
+ action: 'some-unknown-action',
125
+ repository: { full_name: 'acme/app' },
126
+ sender: { login: 'bot' }
127
+ };
128
+ const event = normalizeWebhookEvent(body, 'acme');
129
+
130
+ assert.equal(event.type, 'webhook');
131
+ assert.equal(event.source.kind, 'WebhookDelivery');
132
+ assert.equal(event.source.name, 'unknown');
133
+ assert.equal(event.repository, 'acme/app');
134
+ assert.equal(event.actor, 'bot');
135
+ assert.equal(event.payload, body);
136
+ });
137
+
138
+ // 8. Empty/minimal payload -> doesn't crash
139
+ it('handles empty payload without crashing', () => {
140
+ const event = normalizeWebhookEvent({}, 'acme');
141
+
142
+ assert.equal(event.type, 'webhook');
143
+ assert.equal(event.source.kind, 'WebhookDelivery');
144
+ assert.equal(event.source.name, 'unknown');
145
+ assert.equal(event.repository, '');
146
+ assert.equal(event.actor, 'system');
147
+ assert.equal(event.ref, 'main');
148
+ });
149
+
150
+ it('handles minimal payload with only action without crashing', () => {
151
+ const event = normalizeWebhookEvent({ action: 'opened' }, 'acme');
152
+
153
+ assert.equal(event.type, 'webhook');
154
+ assert.equal(event.source.kind, 'WebhookDelivery');
155
+ });
156
+
157
+ // Additional: label on PR (not issue)
158
+ it('normalizes label on PR with kind PullRequest', () => {
159
+ const body = {
160
+ action: 'labeled',
161
+ label: { name: 'ready-to-merge' },
162
+ pull_request: { number: 99 },
163
+ repository: { full_name: 'acme/app' },
164
+ sender: { login: 'lead' }
165
+ };
166
+ const event = normalizeWebhookEvent(body, 'acme');
167
+
168
+ assert.equal(event.type, 'label-added');
169
+ assert.equal(event.source.kind, 'PullRequest');
170
+ assert.equal(event.source.name, '99');
171
+ assert.deepEqual(event.payload, { label: 'ready-to-merge' });
172
+ });
173
+
174
+ // Additional: issue opened (not PR)
175
+ it('normalizes issue opened to issue-created', () => {
176
+ const body = {
177
+ action: 'opened',
178
+ issue: { number: 33 },
179
+ repository: { full_name: 'acme/app' },
180
+ sender: { login: 'reporter' }
181
+ };
182
+ const event = normalizeWebhookEvent(body, 'acme');
183
+
184
+ assert.equal(event.type, 'issue-created');
185
+ assert.equal(event.source.kind, 'Issue');
186
+ assert.equal(event.source.name, '33');
187
+ assert.equal(event.repository, 'acme/app');
188
+ assert.equal(event.actor, 'reporter');
189
+ });
190
+ });
@@ -0,0 +1,245 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ validateCronExpression,
5
+ calculateNextRun,
6
+ validateWebhookTrigger,
7
+ validateCommentTrigger,
8
+ validateLabelTrigger,
9
+ getTriggerSourceType,
10
+ validateTriggerRule,
11
+ createResource,
12
+ } from '../src/index.js';
13
+
14
+ // ── Cron validation ──────────────────────────────────────────────────────────
15
+
16
+ test('validateCronExpression: accepts valid 5-field cron expression', () => {
17
+ const result = validateCronExpression('*/5 * * * *');
18
+ assert.equal(result.valid, true);
19
+ assert.equal(result.error, undefined);
20
+ });
21
+
22
+ test('validateCronExpression: accepts complex valid cron (fixed values + ranges)', () => {
23
+ const result = validateCronExpression('0 9-17 * * 1-5');
24
+ assert.equal(result.valid, true);
25
+ });
26
+
27
+ test('validateCronExpression: rejects invalid cron string (non-cron words)', () => {
28
+ const result = validateCronExpression('not a cron');
29
+ assert.equal(result.valid, false);
30
+ assert.ok(typeof result.error === 'string' && result.error.length > 0, 'Should have an error message');
31
+ });
32
+
33
+ test('validateCronExpression: rejects empty string', () => {
34
+ const result = validateCronExpression('');
35
+ assert.equal(result.valid, false);
36
+ assert.ok(result.error, 'Should have an error message');
37
+ });
38
+
39
+ test('validateCronExpression: rejects fewer than 5 fields', () => {
40
+ const result = validateCronExpression('* * * *');
41
+ assert.equal(result.valid, false);
42
+ assert.ok(result.error.includes('5'), 'Error should mention 5-field requirement');
43
+ });
44
+
45
+ test('validateCronExpression: rejects more than 5 fields', () => {
46
+ const result = validateCronExpression('* * * * * *');
47
+ assert.equal(result.valid, false);
48
+ assert.ok(result.error.includes('5'), 'Error should mention 5-field requirement');
49
+ });
50
+
51
+ // ── calculateNextRun ─────────────────────────────────────────────────────────
52
+
53
+ test('calculateNextRun: returns a future Date for a valid cron expression', () => {
54
+ const fromDate = new Date('2024-01-01T00:00:00Z');
55
+ const next = calculateNextRun('*/5 * * * *', fromDate);
56
+ assert.ok(next instanceof Date, 'Should return a Date');
57
+ assert.ok(next > fromDate, 'Returned date should be in the future relative to fromDate');
58
+ });
59
+
60
+ test('calculateNextRun: uses current date when fromDate is omitted', () => {
61
+ const before = new Date();
62
+ const next = calculateNextRun('0 0 * * *');
63
+ assert.ok(next instanceof Date, 'Should return a Date');
64
+ assert.ok(next > before, 'Returned date should be after invocation time');
65
+ });
66
+
67
+ test('calculateNextRun: returns null for invalid cron expression', () => {
68
+ const result = calculateNextRun('not-a-cron');
69
+ assert.equal(result, null);
70
+ });
71
+
72
+ // ── validateWebhookTrigger ───────────────────────────────────────────────────
73
+
74
+ test('validateWebhookTrigger: accepts valid webhook config with url and secretRef', () => {
75
+ const config = { url: 'https://example.com/webhook', secretRef: 'my-webhook-secret' };
76
+ const result = validateWebhookTrigger(config);
77
+ assert.equal(result.valid, true);
78
+ assert.equal(result.error, undefined);
79
+ });
80
+
81
+ test('validateWebhookTrigger: rejects config missing url', () => {
82
+ const config = { secretRef: 'my-webhook-secret' };
83
+ const result = validateWebhookTrigger(config);
84
+ assert.equal(result.valid, false);
85
+ assert.ok(result.error.toLowerCase().includes('url'), 'Error should mention missing url');
86
+ });
87
+
88
+ test('validateWebhookTrigger: rejects config with non-http url scheme', () => {
89
+ const config = { url: 'ftp://example.com/webhook', secretRef: 'my-webhook-secret' };
90
+ const result = validateWebhookTrigger(config);
91
+ assert.equal(result.valid, false);
92
+ assert.ok(result.error, 'Should have an error message');
93
+ });
94
+
95
+ test('validateWebhookTrigger: accepts webhook config without secretRef (optional)', () => {
96
+ const config = { url: 'https://example.com/webhook' };
97
+ const result = validateWebhookTrigger(config);
98
+ assert.equal(result.valid, true);
99
+ });
100
+
101
+ // ── validateCommentTrigger ───────────────────────────────────────────────────
102
+
103
+ test('validateCommentTrigger: accepts valid comment trigger with pattern and repos', () => {
104
+ const config = { pattern: '/run-agent', repos: ['myorg/myrepo'] };
105
+ const result = validateCommentTrigger(config);
106
+ assert.equal(result.valid, true);
107
+ assert.equal(result.error, undefined);
108
+ });
109
+
110
+ test('validateCommentTrigger: accepts comment trigger without repos (global scope)', () => {
111
+ const config = { pattern: '/deploy' };
112
+ const result = validateCommentTrigger(config);
113
+ assert.equal(result.valid, true);
114
+ });
115
+
116
+ test('validateCommentTrigger: rejects empty pattern', () => {
117
+ const config = { pattern: '', repos: ['myorg/myrepo'] };
118
+ const result = validateCommentTrigger(config);
119
+ assert.equal(result.valid, false);
120
+ assert.ok(result.error.toLowerCase().includes('pattern'), 'Error should mention pattern');
121
+ });
122
+
123
+ test('validateCommentTrigger: rejects missing pattern field', () => {
124
+ const config = { repos: ['myorg/myrepo'] };
125
+ const result = validateCommentTrigger(config);
126
+ assert.equal(result.valid, false);
127
+ assert.ok(result.error, 'Should have an error message');
128
+ });
129
+
130
+ // ── validateLabelTrigger ─────────────────────────────────────────────────────
131
+
132
+ test('validateLabelTrigger: accepts valid label config with labels array and action', () => {
133
+ const config = { labels: ['bug', 'urgent'], action: 'labeled' };
134
+ const result = validateLabelTrigger(config);
135
+ assert.equal(result.valid, true);
136
+ assert.equal(result.error, undefined);
137
+ });
138
+
139
+ test('validateLabelTrigger: accepts label config with action "unlabeled"', () => {
140
+ const config = { labels: ['wontfix'], action: 'unlabeled' };
141
+ const result = validateLabelTrigger(config);
142
+ assert.equal(result.valid, true);
143
+ });
144
+
145
+ test('validateLabelTrigger: rejects empty labels array', () => {
146
+ const config = { labels: [], action: 'labeled' };
147
+ const result = validateLabelTrigger(config);
148
+ assert.equal(result.valid, false);
149
+ assert.ok(result.error.toLowerCase().includes('label'), 'Error should mention labels');
150
+ });
151
+
152
+ test('validateLabelTrigger: rejects missing labels field', () => {
153
+ const config = { action: 'labeled' };
154
+ const result = validateLabelTrigger(config);
155
+ assert.equal(result.valid, false);
156
+ assert.ok(result.error, 'Should have an error message');
157
+ });
158
+
159
+ test('validateLabelTrigger: rejects invalid action value', () => {
160
+ const config = { labels: ['bug'], action: 'deleted' };
161
+ const result = validateLabelTrigger(config);
162
+ assert.equal(result.valid, false);
163
+ assert.ok(result.error.toLowerCase().includes('action'), 'Error should mention action');
164
+ });
165
+
166
+ // ── getTriggerSourceType ─────────────────────────────────────────────────────
167
+
168
+ test('getTriggerSourceType: returns "cron" for rule with cronExpression', () => {
169
+ const rule = { spec: { cronExpression: '*/5 * * * *' } };
170
+ assert.equal(getTriggerSourceType(rule), 'cron');
171
+ });
172
+
173
+ test('getTriggerSourceType: returns "webhook" for rule with webhookTrigger', () => {
174
+ const rule = { spec: { webhookTrigger: { url: 'https://example.com/hook' } } };
175
+ assert.equal(getTriggerSourceType(rule), 'webhook');
176
+ });
177
+
178
+ test('getTriggerSourceType: returns "comment" for rule with commentTrigger', () => {
179
+ const rule = { spec: { commentTrigger: { pattern: '/run' } } };
180
+ assert.equal(getTriggerSourceType(rule), 'comment');
181
+ });
182
+
183
+ test('getTriggerSourceType: returns "label" for rule with labelTrigger', () => {
184
+ const rule = { spec: { labelTrigger: { labels: ['bug'], action: 'labeled' } } };
185
+ assert.equal(getTriggerSourceType(rule), 'label');
186
+ });
187
+
188
+ test('getTriggerSourceType: returns "event" for rule with only sources array', () => {
189
+ const rule = { spec: { sources: ['ci-failure'] } };
190
+ assert.equal(getTriggerSourceType(rule), 'event');
191
+ });
192
+
193
+ test('getTriggerSourceType: returns "unknown" for rule with no recognized source spec', () => {
194
+ const rule = { spec: {} };
195
+ assert.equal(getTriggerSourceType(rule), 'unknown');
196
+ });
197
+
198
+ // ── validateTriggerRule with cron source ─────────────────────────────────────
199
+
200
+ test('validateTriggerRule: rule with valid cron source passes validation', () => {
201
+ const rule = createResource(
202
+ 'AgentTriggerRule',
203
+ { name: 'cron-rule', namespace: 'krate-org-default' },
204
+ {
205
+ organizationRef: 'default',
206
+ cronExpression: '0 */6 * * *',
207
+ agentStack: 'maintenance-stack',
208
+ taskKind: 'maintenance',
209
+ }
210
+ );
211
+ const result = validateTriggerRule(rule);
212
+ assert.equal(result.valid, true);
213
+ assert.equal(result.errors.length, 0);
214
+ });
215
+
216
+ test('validateTriggerRule: rule with invalid cron expression fails validation', () => {
217
+ const rule = createResource(
218
+ 'AgentTriggerRule',
219
+ { name: 'bad-cron-rule', namespace: 'krate-org-default' },
220
+ {
221
+ organizationRef: 'default',
222
+ cronExpression: 'every five minutes',
223
+ agentStack: 'maintenance-stack',
224
+ taskKind: 'maintenance',
225
+ }
226
+ );
227
+ const result = validateTriggerRule(rule);
228
+ assert.equal(result.valid, false);
229
+ assert.ok(result.errors.length > 0, 'Should have validation errors');
230
+ });
231
+
232
+ test('validateTriggerRule: rule with webhook source passes validation', () => {
233
+ const rule = createResource(
234
+ 'AgentTriggerRule',
235
+ { name: 'webhook-rule', namespace: 'krate-org-default' },
236
+ {
237
+ organizationRef: 'default',
238
+ webhookTrigger: { url: 'https://example.com/hook', secretRef: 'hook-secret' },
239
+ agentStack: 'webhook-stack',
240
+ taskKind: 'on-demand',
241
+ }
242
+ );
243
+ const result = validateTriggerRule(rule);
244
+ assert.equal(result.valid, true);
245
+ });
@@ -0,0 +1,181 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentWorkspaceController, createResource } from '../src/index.js';
4
+
5
+ function makeWorkspace(name, repository, runRef, phase = 'InUse', extra = {}) {
6
+ const workspace = createResource('KrateWorkspace', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: 'default',
8
+ repository,
9
+ volumeSpec: extra.volumeSpec || { storageClassName: 'standard', capacity: '10Gi', accessModes: ['ReadWriteOnce'] },
10
+ branch: extra.branch || 'main',
11
+ pvcName: extra.pvcName || `krate-ws-${name}`,
12
+ });
13
+ workspace.status = { phase, createdAt: new Date().toISOString(), runRef: runRef || undefined, volumeStatus: 'Bound', ...extra.status };
14
+ return workspace;
15
+ }
16
+
17
+ function makeRuntime(name, workspaceRef, status = 'provisioning') {
18
+ const runtime = createResource('KrateWorkspaceRuntime', { name, namespace: 'krate-org-default' }, {
19
+ organizationRef: 'default',
20
+ workspaceRef,
21
+ status
22
+ });
23
+ runtime.status = { phase: 'Provisioning', createdAt: new Date().toISOString() };
24
+ return runtime;
25
+ }
26
+
27
+ test('provisionWorkspace creates KrateWorkspace + KrateWorkspaceRuntime', () => {
28
+ const controller = createAgentWorkspaceController();
29
+ const result = controller.provisionWorkspace({
30
+ repository: 'my-repo',
31
+ ref: 'abc123',
32
+ branch: 'feature-1',
33
+ dispatchRun: 'run-1',
34
+ namespace: 'krate-org-default',
35
+ organizationRef: 'default'
36
+ });
37
+
38
+ assert.equal(result.error, false, 'Should succeed');
39
+ assert.ok(result.workspace, 'Should return a workspace resource');
40
+ assert.equal(result.workspace.kind, 'KrateWorkspace');
41
+ assert.equal(result.workspace.spec.repository, 'my-repo');
42
+ assert.equal(result.workspace.status.runRef, 'run-1');
43
+ assert.equal(result.workspace.status.phase, 'InUse');
44
+ assert.ok(result.pvcManifest, 'Should return a PVC manifest');
45
+
46
+ assert.ok(result.runtime, 'Should return a runtime resource');
47
+ assert.equal(result.runtime.kind, 'KrateWorkspaceRuntime');
48
+ assert.equal(result.runtime.spec.workspaceRef, result.workspace.metadata.name);
49
+ assert.equal(result.runtime.spec.status, 'provisioning');
50
+ assert.equal(result.runtime.status.phase, 'Provisioning');
51
+ });
52
+
53
+ test('archiveWorkspace sets phase=Archived', () => {
54
+ const controller = createAgentWorkspaceController();
55
+ const existing = makeWorkspace('ws-1', 'my-repo', 'run-1', 'InUse');
56
+ const result = controller.archiveWorkspace({
57
+ workspaceName: 'ws-1',
58
+ reason: 'Run completed',
59
+ resources: { KrateWorkspace: [existing] }
60
+ });
61
+
62
+ assert.equal(result.error, false, 'Should succeed');
63
+ assert.equal(result.workspace.status.phase, 'Archived');
64
+ assert.ok(result.workspace.status.archivedAt, 'Should have archivedAt timestamp');
65
+ assert.equal(result.workspace.status.archiveReason, 'Run completed');
66
+ });
67
+
68
+ test('recoverWorkspace sets phase=Active', () => {
69
+ const controller = createAgentWorkspaceController();
70
+ const archived = makeWorkspace('ws-2', 'my-repo', 'run-1', 'Archived', {
71
+ status: { archivedAt: new Date().toISOString(), archiveReason: 'Cleanup' }
72
+ });
73
+ const result = controller.recoverWorkspace({
74
+ workspaceName: 'ws-2',
75
+ resources: { KrateWorkspace: [archived] }
76
+ });
77
+
78
+ assert.equal(result.error, false, 'Should succeed');
79
+ assert.equal(result.workspace.status.phase, 'Active');
80
+ assert.equal(result.workspace.status.archivedAt, undefined, 'archivedAt should be cleared');
81
+ assert.equal(result.workspace.status.archiveReason, undefined, 'archiveReason should be cleared');
82
+ });
83
+
84
+ test('bindSession adds session to boundSessions', () => {
85
+ const controller = createAgentWorkspaceController();
86
+ const existing = makeWorkspace('ws-3', 'my-repo', 'run-2', 'InUse');
87
+ const result = controller.bindSession({
88
+ workspaceName: 'ws-3',
89
+ sessionRef: 'session-1',
90
+ agent: 'code-agent',
91
+ namespace: 'krate-org-default',
92
+ organizationRef: 'default',
93
+ resources: { KrateWorkspace: [existing] }
94
+ });
95
+
96
+ assert.equal(result.error, false, 'Should succeed');
97
+ assert.ok(result.workspace, 'Should return updated workspace');
98
+ assert.equal(result.workspace.status.boundSessions.length, 1, 'Should have one bound session');
99
+ assert.equal(result.workspace.status.boundSessions[0].sessionRef, 'session-1');
100
+ assert.equal(result.workspace.status.boundSessions[0].agent, 'code-agent');
101
+ assert.ok(result.workspace.status.boundSessions[0].boundAt, 'Should have boundAt timestamp');
102
+ });
103
+
104
+ test('linkWorkItem creates WorkItemWorkspaceLink', () => {
105
+ const controller = createAgentWorkspaceController();
106
+ const result = controller.linkWorkItem({
107
+ workspaceName: 'ws-4',
108
+ workItemRef: 'issue-42',
109
+ workItemKind: 'Issue',
110
+ namespace: 'krate-org-default',
111
+ organizationRef: 'default'
112
+ });
113
+
114
+ assert.equal(result.error, false, 'Should succeed');
115
+ assert.ok(result.link, 'Should return a link resource');
116
+ assert.equal(result.link.kind, 'WorkItemWorkspaceLink');
117
+ assert.equal(result.link.spec.workItemRef, 'issue-42');
118
+ assert.equal(result.link.spec.workItemKind, 'Issue');
119
+ assert.equal(result.link.spec.workspace, 'ws-4');
120
+ assert.equal(result.link.spec.organizationRef, 'default');
121
+ });
122
+
123
+ test('linkWorkItemToSession creates WorkItemSessionLink', () => {
124
+ const controller = createAgentWorkspaceController();
125
+ const result = controller.linkWorkItemToSession({
126
+ workItemRef: 'pr-10',
127
+ workItemKind: 'PullRequest',
128
+ sessionRef: 'session-5',
129
+ namespace: 'krate-org-default',
130
+ organizationRef: 'default'
131
+ });
132
+
133
+ assert.equal(result.error, false, 'Should succeed');
134
+ assert.ok(result.link, 'Should return a link resource');
135
+ assert.equal(result.link.kind, 'WorkItemSessionLink');
136
+ assert.equal(result.link.spec.workItemRef, 'pr-10');
137
+ assert.equal(result.link.spec.workItemKind, 'PullRequest');
138
+ assert.equal(result.link.spec.agentSession, 'session-5');
139
+ assert.equal(result.link.spec.organizationRef, 'default');
140
+ });
141
+
142
+ test('listWorkspacesForRepo filters by repository', () => {
143
+ const controller = createAgentWorkspaceController();
144
+ const ws1 = makeWorkspace('ws-a', 'repo-alpha', 'run-a');
145
+ const ws2 = makeWorkspace('ws-b', 'repo-beta', 'run-b');
146
+ const ws3 = makeWorkspace('ws-c', 'repo-alpha', 'run-c');
147
+
148
+ const result = controller.listWorkspacesForRepo({
149
+ repository: 'repo-alpha',
150
+ resources: { KrateWorkspace: [ws1, ws2, ws3] }
151
+ });
152
+
153
+ assert.equal(result.length, 2, 'Should return workspaces for repo-alpha only');
154
+ assert.ok(result.every((w) => w.spec.repository === 'repo-alpha'), 'All should belong to repo-alpha');
155
+ });
156
+
157
+ test('archiveWorkspace on nonexistent returns error', () => {
158
+ const controller = createAgentWorkspaceController();
159
+ const result = controller.archiveWorkspace({
160
+ workspaceName: 'ws-nonexistent',
161
+ reason: 'Cleanup',
162
+ resources: { KrateWorkspace: [] }
163
+ });
164
+
165
+ assert.equal(result.error, true, 'Should fail');
166
+ assert.equal(result.reason, 'not-found');
167
+ assert.ok(result.message.includes('ws-nonexistent'), 'Message should mention the workspace name');
168
+ });
169
+
170
+ test('recoverWorkspace on non-archived returns error', () => {
171
+ const controller = createAgentWorkspaceController();
172
+ const active = makeWorkspace('ws-active', 'my-repo', 'run-x', 'InUse');
173
+ const result = controller.recoverWorkspace({
174
+ workspaceName: 'ws-active',
175
+ resources: { KrateWorkspace: [active] }
176
+ });
177
+
178
+ assert.equal(result.error, true, 'Should fail');
179
+ assert.equal(result.reason, 'not-archived');
180
+ assert.ok(result.message.includes('not archived'), 'Message should indicate not archived');
181
+ });