@a5c-ai/krate 5.0.1-staging.04a3db697

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 (246) 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 +3067 -0
  6. package/dist/krate-lifecycle.json +201 -0
  7. package/dist/krate-runtime-snapshot.json +2955 -0
  8. package/dist/krate-summary.json +722 -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/product-requirements.md +62 -0
  108. package/docs/roadmap-mvp.md +87 -0
  109. package/docs/system-requirements.md +90 -0
  110. package/docs/tests/README.md +53 -0
  111. package/docs/tests/agent-qa-plan.md +63 -0
  112. package/docs/tests/browser-ui-tests.md +62 -0
  113. package/docs/tests/ci-quality-gates.md +48 -0
  114. package/docs/tests/coverage-model.md +64 -0
  115. package/docs/tests/e2e-scenario-tests.md +53 -0
  116. package/docs/tests/fixtures-test-data.md +63 -0
  117. package/docs/tests/observability-reliability-tests.md +54 -0
  118. package/docs/tests/product-test-matrix.md +145 -0
  119. package/docs/tests/qa-adoption-roadmap.md +130 -0
  120. package/docs/tests/qa-automation-plan.md +101 -0
  121. package/docs/tests/security-compliance-tests.md +57 -0
  122. package/docs/tests/test-framework-tools.md +88 -0
  123. package/docs/tests/test-suite-layout.md +121 -0
  124. package/docs/tests/unit-integration-tests.md +48 -0
  125. package/docs/todo-kyverno +714 -0
  126. package/docs/todos.md +4 -0
  127. package/docs/user-stories.md +78 -0
  128. package/examples/minikube-demo.yaml +190 -0
  129. package/examples/oam-application.yaml +23 -0
  130. package/examples/policy-kyverno-pr-title.yaml +18 -0
  131. package/package.json +63 -0
  132. package/scripts/build.mjs +29 -0
  133. package/scripts/setup-minikube.mjs +65 -0
  134. package/scripts/smoke.mjs +37 -0
  135. package/scripts/validate-doc-coverage.mjs +152 -0
  136. package/scripts/validate-package.mjs +93 -0
  137. package/scripts/validate-ui.mjs +236 -0
  138. package/src/agent-adapter-controller.js +169 -0
  139. package/src/agent-approval-controller.js +170 -0
  140. package/src/agent-context-bundles.js +242 -0
  141. package/src/agent-dispatch-controller.js +209 -0
  142. package/src/agent-gateway-config-controller.js +147 -0
  143. package/src/agent-memory-controller.js +357 -0
  144. package/src/agent-memory-import.js +327 -0
  145. package/src/agent-memory-query.js +292 -0
  146. package/src/agent-memory-repository-source-controller.js +255 -0
  147. package/src/agent-mux-client.js +280 -0
  148. package/src/agent-permission-review.js +250 -0
  149. package/src/agent-project-controller.js +117 -0
  150. package/src/agent-provider-config-controller.js +150 -0
  151. package/src/agent-secret-config-grant-controller.js +282 -0
  152. package/src/agent-session-transcript-controller.js +189 -0
  153. package/src/agent-stack-controller.js +347 -0
  154. package/src/agent-subagent-controller.js +160 -0
  155. package/src/agent-transport-binding-controller.js +121 -0
  156. package/src/agent-trigger-controller.js +321 -0
  157. package/src/agent-workspace-controller.js +447 -0
  158. package/src/agent-writeback-controller.js +302 -0
  159. package/src/api-controller.js +541 -0
  160. package/src/argocd-gitops.js +43 -0
  161. package/src/async-controller.js +207 -0
  162. package/src/audit-controller.js +191 -0
  163. package/src/auth.js +307 -0
  164. package/src/component-catalog.js +41 -0
  165. package/src/control-plane.js +136 -0
  166. package/src/controller-client.js +50 -0
  167. package/src/controller-ui.js +551 -0
  168. package/src/data-plane.js +178 -0
  169. package/src/event-bus.js +61 -0
  170. package/src/external/conflict-controller.js +225 -0
  171. package/src/external/github/auth.js +96 -0
  172. package/src/external/github/cicd.js +180 -0
  173. package/src/external/github/git-forge.js +240 -0
  174. package/src/external/github/index.js +144 -0
  175. package/src/external/github/issue-tracking.js +163 -0
  176. package/src/external/provider-adapter.js +161 -0
  177. package/src/external/provider-resource-factory.js +161 -0
  178. package/src/external/sync-controller.js +235 -0
  179. package/src/external/webhook-controller.js +144 -0
  180. package/src/external/write-controller.js +283 -0
  181. package/src/gitea-backend.js +95 -0
  182. package/src/gitea-service.js +173 -0
  183. package/src/handoff.js +98 -0
  184. package/src/hooks-events.js +63 -0
  185. package/src/http-server.js +377 -0
  186. package/src/identity-policy.js +86 -0
  187. package/src/index.js +55 -0
  188. package/src/kubernetes-controller-async.js +511 -0
  189. package/src/kubernetes-controller.js +878 -0
  190. package/src/kubernetes-resource-gateway.js +48 -0
  191. package/src/operations.js +112 -0
  192. package/src/org-scoping.js +5 -0
  193. package/src/resource-model.js +221 -0
  194. package/src/runners-ci.js +48 -0
  195. package/src/runtime.js +196 -0
  196. package/src/snapshot-cache.js +157 -0
  197. package/src/web-ui.js +40 -0
  198. package/tests/agent-adapter-controller.test.js +361 -0
  199. package/tests/agent-approval-controller.test.js +173 -0
  200. package/tests/agent-context-bundles.test.js +278 -0
  201. package/tests/agent-dispatch-controller.test.js +315 -0
  202. package/tests/agent-gateway-config-controller.test.js +386 -0
  203. package/tests/agent-memory-controller.test.js +308 -0
  204. package/tests/agent-memory-import-snapshot.test.js +477 -0
  205. package/tests/agent-memory-query.test.js +404 -0
  206. package/tests/agent-memory-repository-source.test.js +514 -0
  207. package/tests/agent-mux-client.test.js +204 -0
  208. package/tests/agent-permission-review-v2.test.js +317 -0
  209. package/tests/agent-permission-review.test.js +209 -0
  210. package/tests/agent-project-controller.test.js +302 -0
  211. package/tests/agent-provider-config-controller.test.js +376 -0
  212. package/tests/agent-resources.test.js +228 -0
  213. package/tests/agent-secret-config-grant.test.js +231 -0
  214. package/tests/agent-session-transcript-controller.test.js +499 -0
  215. package/tests/agent-stack-controller.test.js +221 -0
  216. package/tests/agent-subagent-controller.test.js +201 -0
  217. package/tests/agent-transport-binding-controller.test.js +294 -0
  218. package/tests/agent-trigger-controller.test.js +211 -0
  219. package/tests/agent-trigger-routes.test.js +190 -0
  220. package/tests/agent-trigger-sources.test.js +245 -0
  221. package/tests/agent-workspace-controller.test.js +181 -0
  222. package/tests/agent-writeback.test.js +292 -0
  223. package/tests/approval-persistence.test.js +171 -0
  224. package/tests/async-controller.test.js +252 -0
  225. package/tests/audit-controller.test.js +227 -0
  226. package/tests/deployment.test.js +396 -0
  227. package/tests/e2e/lifecycle.test.js +117 -0
  228. package/tests/external-github-forge.test.js +560 -0
  229. package/tests/external-github-issues-cicd.test.js +520 -0
  230. package/tests/external-integration.test.js +470 -0
  231. package/tests/external-persistence.test.js +340 -0
  232. package/tests/external-provider-adapter.test.js +365 -0
  233. package/tests/external-resource-model.test.js +215 -0
  234. package/tests/external-webhook-sync.test.js +287 -0
  235. package/tests/external-write-conflict.test.js +353 -0
  236. package/tests/gitea-service.test.js +253 -0
  237. package/tests/health-check-real.test.js +165 -0
  238. package/tests/integration/full-flow.test.js +266 -0
  239. package/tests/krate.test.js +727 -0
  240. package/tests/memory-search-wiring.test.js +270 -0
  241. package/tests/org-scoping.test.js +687 -0
  242. package/tests/session-cookie-hmac.test.js +151 -0
  243. package/tests/snapshot-performance.test.js +247 -0
  244. package/tests/sse-events.test.js +107 -0
  245. package/tests/workspace-volumes.test.js +312 -0
  246. package/tests/writeback-persistence.test.js +207 -0
@@ -0,0 +1,520 @@
1
+ import assert from 'node:assert/strict';
2
+ import { describe, it } from 'node:test';
3
+ import {
4
+ GitHubIssueTracking,
5
+ GitHubCicd,
6
+ createGitHubProvider,
7
+ GITHUB_ISSUE_TRACKING_BOUNDARY,
8
+ GITHUB_CICD_BOUNDARY
9
+ } from '../src/external/github/index.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Test Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function makeMockFetch(responses) {
16
+ let callIndex = 0;
17
+ return async function mockFetch(url, options = {}) {
18
+ const entry = Array.isArray(responses) ? responses[callIndex++] : responses;
19
+ if (!entry) throw new Error(`Unexpected fetch call to ${url}`);
20
+ return {
21
+ ok: entry.status >= 200 && entry.status < 300,
22
+ status: entry.status ?? 200,
23
+ json: async () => entry.body,
24
+ text: async () => JSON.stringify(entry.body)
25
+ };
26
+ };
27
+ }
28
+
29
+ function makeIssueTracking(fetchResponses = []) {
30
+ return new GitHubIssueTracking({
31
+ owner: 'my-org',
32
+ installationToken: 'ghs_test_token',
33
+ fetchImpl: makeMockFetch(fetchResponses)
34
+ });
35
+ }
36
+
37
+ function makeCicd(fetchResponses = []) {
38
+ return new GitHubCicd({
39
+ owner: 'my-org',
40
+ installationToken: 'ghs_test_token',
41
+ fetchImpl: makeMockFetch(fetchResponses)
42
+ });
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // 1. GitHubIssueTracking.listIssues — returns normalized issue list
47
+ // ---------------------------------------------------------------------------
48
+
49
+ describe('GitHubIssueTracking.listIssues', () => {
50
+ it('returns an array of normalized issue objects', async () => {
51
+ const tracker = makeIssueTracking({
52
+ status: 200,
53
+ body: [
54
+ { id: 1, number: 10, title: 'Bug report', state: 'open', body: 'Something is broken', html_url: 'https://github.com/my-org/repo/issues/10', user: { login: 'alice' }, labels: [{ name: 'bug' }] },
55
+ { id: 2, number: 11, title: 'Feature request', state: 'closed', body: 'Add dark mode', html_url: 'https://github.com/my-org/repo/issues/11', user: { login: 'bob' }, labels: [] }
56
+ ]
57
+ });
58
+
59
+ const issues = await tracker.listIssues({ repo: 'my-repo' });
60
+
61
+ assert.ok(Array.isArray(issues), 'listIssues must return an array');
62
+ assert.equal(issues.length, 2, 'must return both issues');
63
+ assert.equal(issues[0].number, 10, 'first issue number must match');
64
+ assert.equal(issues[0].title, 'Bug report', 'first issue title must match');
65
+ assert.equal(issues[0].state, 'open', 'issue state must be present');
66
+ assert.ok(Array.isArray(issues[0].labels), 'labels must be an array');
67
+ assert.equal(issues[0].labels[0], 'bug', 'label name must be extracted');
68
+ assert.ok(issues[0].htmlUrl, 'htmlUrl must be present');
69
+ });
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // 2. GitHubIssueTracking.createIssue — sends title and body
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe('GitHubIssueTracking.createIssue', () => {
77
+ it('sends POST with title and body, returns normalized issue', async () => {
78
+ const calls = [];
79
+ const tracker = new GitHubIssueTracking({
80
+ owner: 'my-org',
81
+ installationToken: 'ghs_test',
82
+ fetchImpl: async (url, options = {}) => {
83
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
84
+ return {
85
+ ok: true, status: 201,
86
+ json: async () => ({ id: 42, number: 5, title: 'New issue', state: 'open', body: 'Issue body', html_url: 'https://github.com/my-org/repo/issues/5', user: { login: 'alice' }, labels: [] })
87
+ };
88
+ }
89
+ });
90
+
91
+ const result = await tracker.createIssue({ repo: 'my-repo', title: 'New issue', body: 'Issue body' });
92
+
93
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
94
+ assert.equal(calls[0].method, 'POST', 'must use POST method');
95
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/issues'), `URL must target issues endpoint, got ${calls[0].url}`);
96
+ assert.equal(calls[0].body.title, 'New issue', 'payload must include title');
97
+ assert.equal(calls[0].body.body, 'Issue body', 'payload must include body');
98
+ assert.ok(result, 'createIssue must return the created issue');
99
+ assert.equal(result.number, 5, 'result must include issue number');
100
+ });
101
+ });
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // 3. GitHubIssueTracking.updateIssue — sends patch
105
+ // ---------------------------------------------------------------------------
106
+
107
+ describe('GitHubIssueTracking.updateIssue', () => {
108
+ it('sends PATCH to the issue endpoint with updated fields', async () => {
109
+ const calls = [];
110
+ const tracker = new GitHubIssueTracking({
111
+ owner: 'my-org',
112
+ installationToken: 'ghs_test',
113
+ fetchImpl: async (url, options = {}) => {
114
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
115
+ return {
116
+ ok: true, status: 200,
117
+ json: async () => ({ id: 1, number: 7, title: 'Updated title', state: 'open', body: 'Updated body', html_url: '', user: { login: 'alice' }, labels: [] })
118
+ };
119
+ }
120
+ });
121
+
122
+ const result = await tracker.updateIssue({ repo: 'my-repo', issueNumber: 7, title: 'Updated title', body: 'Updated body' });
123
+
124
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
125
+ assert.equal(calls[0].method, 'PATCH', 'must use PATCH method');
126
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/issues/7'), `URL must target issue 7, got ${calls[0].url}`);
127
+ assert.equal(calls[0].body.title, 'Updated title', 'payload must include updated title');
128
+ assert.equal(calls[0].body.body, 'Updated body', 'payload must include updated body');
129
+ assert.ok(result, 'updateIssue must return the updated issue');
130
+ assert.equal(result.number, 7, 'result number must match');
131
+ });
132
+ });
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // 4. GitHubIssueTracking.closeIssue — sets state to closed
136
+ // ---------------------------------------------------------------------------
137
+
138
+ describe('GitHubIssueTracking.closeIssue', () => {
139
+ it('sends PATCH with state=closed to the issue endpoint', async () => {
140
+ const calls = [];
141
+ const tracker = new GitHubIssueTracking({
142
+ owner: 'my-org',
143
+ installationToken: 'ghs_test',
144
+ fetchImpl: async (url, options = {}) => {
145
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
146
+ return {
147
+ ok: true, status: 200,
148
+ json: async () => ({ id: 1, number: 3, title: 'Old issue', state: 'closed', body: '', html_url: '', user: { login: 'alice' }, labels: [] })
149
+ };
150
+ }
151
+ });
152
+
153
+ const result = await tracker.closeIssue({ repo: 'my-repo', issueNumber: 3 });
154
+
155
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
156
+ assert.equal(calls[0].method, 'PATCH', 'must use PATCH method');
157
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/issues/3'), `URL must target issue 3, got ${calls[0].url}`);
158
+ assert.equal(calls[0].body.state, 'closed', 'payload must set state to closed');
159
+ assert.equal(result.state, 'closed', 'returned issue must have state=closed');
160
+ });
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // 5. GitHubIssueTracking.listComments — returns comments for issue
165
+ // ---------------------------------------------------------------------------
166
+
167
+ describe('GitHubIssueTracking.listComments', () => {
168
+ it('returns an array of normalized comment objects for an issue', async () => {
169
+ const tracker = makeIssueTracking({
170
+ status: 200,
171
+ body: [
172
+ { id: 101, body: 'First comment', user: { login: 'alice' }, created_at: '2025-01-01T00:00:00Z', html_url: 'https://github.com/my-org/repo/issues/5#issuecomment-101' },
173
+ { id: 102, body: 'Second comment', user: { login: 'bob' }, created_at: '2025-01-02T00:00:00Z', html_url: 'https://github.com/my-org/repo/issues/5#issuecomment-102' }
174
+ ]
175
+ });
176
+
177
+ const comments = await tracker.listComments({ repo: 'my-repo', issueNumber: 5 });
178
+
179
+ assert.ok(Array.isArray(comments), 'listComments must return an array');
180
+ assert.equal(comments.length, 2, 'must return both comments');
181
+ assert.equal(comments[0].id, 101, 'first comment id must match');
182
+ assert.equal(comments[0].body, 'First comment', 'comment body must be present');
183
+ assert.equal(comments[0].author, 'alice', 'comment author must be normalized from user.login');
184
+ assert.ok(comments[0].createdAt, 'createdAt must be present');
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // 6. GitHubIssueTracking.createComment — posts comment body
190
+ // ---------------------------------------------------------------------------
191
+
192
+ describe('GitHubIssueTracking.createComment', () => {
193
+ it('sends POST with comment body to the issue comments endpoint', async () => {
194
+ const calls = [];
195
+ const tracker = new GitHubIssueTracking({
196
+ owner: 'my-org',
197
+ installationToken: 'ghs_test',
198
+ fetchImpl: async (url, options = {}) => {
199
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
200
+ return {
201
+ ok: true, status: 201,
202
+ json: async () => ({ id: 200, body: 'Great fix!', user: { login: 'alice' }, created_at: '2025-01-03T00:00:00Z', html_url: '' })
203
+ };
204
+ }
205
+ });
206
+
207
+ const result = await tracker.createComment({ repo: 'my-repo', issueNumber: 5, body: 'Great fix!' });
208
+
209
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
210
+ assert.equal(calls[0].method, 'POST', 'must use POST method');
211
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/issues/5/comments'), `URL must target issue 5 comments, got ${calls[0].url}`);
212
+ assert.equal(calls[0].body.body, 'Great fix!', 'payload must include comment body');
213
+ assert.ok(result, 'createComment must return the created comment');
214
+ assert.equal(result.id, 200, 'returned comment must include id');
215
+ });
216
+ });
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // 7. GitHubIssueTracking — implements issueTracking interface
220
+ // ---------------------------------------------------------------------------
221
+
222
+ describe('GitHubIssueTracking — interface contract', () => {
223
+ it('exposes all required issueTracking interface methods', () => {
224
+ const tracker = makeIssueTracking();
225
+ const requiredMethods = [
226
+ 'listIssues',
227
+ 'createIssue',
228
+ 'updateIssue',
229
+ 'closeIssue',
230
+ 'listComments',
231
+ 'createComment'
232
+ ];
233
+ for (const method of requiredMethods) {
234
+ assert.equal(typeof tracker[method], 'function', `GitHubIssueTracking must expose method: ${method}`);
235
+ }
236
+ });
237
+
238
+ it('has a role property identifying it as github-issue-tracking', () => {
239
+ const tracker = makeIssueTracking();
240
+ assert.equal(tracker.role, 'github-issue-tracking', 'role must be "github-issue-tracking"');
241
+ });
242
+
243
+ it('throws when required constructor arguments are missing', () => {
244
+ assert.throws(
245
+ () => new GitHubIssueTracking({ installationToken: 'tok', fetchImpl: () => {} }),
246
+ /owner/i,
247
+ 'must throw when owner is missing'
248
+ );
249
+ assert.throws(
250
+ () => new GitHubIssueTracking({ owner: 'org', fetchImpl: () => {} }),
251
+ /installationToken/i,
252
+ 'must throw when installationToken is missing'
253
+ );
254
+ });
255
+ });
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // 8. GitHubCicd.listWorkflowRuns — returns normalized run list
259
+ // ---------------------------------------------------------------------------
260
+
261
+ describe('GitHubCicd.listWorkflowRuns', () => {
262
+ it('returns an array of normalized workflow run objects', async () => {
263
+ const cicd = makeCicd({
264
+ status: 200,
265
+ body: {
266
+ workflow_runs: [
267
+ { id: 1001, name: 'CI', status: 'completed', conclusion: 'success', head_branch: 'main', head_sha: 'abc123', html_url: 'https://github.com/my-org/repo/actions/runs/1001', created_at: '2025-01-01T10:00:00Z', updated_at: '2025-01-01T10:05:00Z' },
268
+ { id: 1002, name: 'CI', status: 'in_progress', conclusion: null, head_branch: 'feature-x', head_sha: 'def456', html_url: 'https://github.com/my-org/repo/actions/runs/1002', created_at: '2025-01-02T10:00:00Z', updated_at: '2025-01-02T10:02:00Z' }
269
+ ]
270
+ }
271
+ });
272
+
273
+ const runs = await cicd.listWorkflowRuns({ repo: 'my-repo' });
274
+
275
+ assert.ok(Array.isArray(runs), 'listWorkflowRuns must return an array');
276
+ assert.equal(runs.length, 2, 'must return both workflow runs');
277
+ assert.equal(runs[0].id, 1001, 'first run id must match');
278
+ assert.equal(runs[0].name, 'CI', 'run name must be present');
279
+ assert.equal(runs[0].status, 'completed', 'run status must be present');
280
+ assert.equal(runs[0].conclusion, 'success', 'run conclusion must be present');
281
+ assert.equal(runs[0].headBranch, 'main', 'headBranch must be normalized from head_branch');
282
+ assert.ok(runs[0].htmlUrl, 'htmlUrl must be present');
283
+ });
284
+ });
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // 9. GitHubCicd.listJobs — returns jobs for a run
288
+ // ---------------------------------------------------------------------------
289
+
290
+ describe('GitHubCicd.listJobs', () => {
291
+ it('returns an array of normalized job objects for a workflow run', async () => {
292
+ const cicd = makeCicd({
293
+ status: 200,
294
+ body: {
295
+ jobs: [
296
+ { id: 2001, name: 'build', status: 'completed', conclusion: 'success', started_at: '2025-01-01T10:00:00Z', completed_at: '2025-01-01T10:03:00Z', html_url: 'https://github.com/my-org/repo/actions/runs/1001/jobs/2001' },
297
+ { id: 2002, name: 'test', status: 'completed', conclusion: 'failure', started_at: '2025-01-01T10:03:00Z', completed_at: '2025-01-01T10:05:00Z', html_url: 'https://github.com/my-org/repo/actions/runs/1001/jobs/2002' }
298
+ ]
299
+ }
300
+ });
301
+
302
+ const jobs = await cicd.listJobs({ repo: 'my-repo', runId: 1001 });
303
+
304
+ assert.ok(Array.isArray(jobs), 'listJobs must return an array');
305
+ assert.equal(jobs.length, 2, 'must return both jobs');
306
+ assert.equal(jobs[0].id, 2001, 'first job id must match');
307
+ assert.equal(jobs[0].name, 'build', 'job name must be present');
308
+ assert.equal(jobs[0].status, 'completed', 'job status must be present');
309
+ assert.equal(jobs[0].conclusion, 'success', 'job conclusion must be present');
310
+ assert.ok(jobs[0].startedAt, 'startedAt must be present');
311
+ assert.ok(jobs[0].htmlUrl, 'htmlUrl must be present');
312
+ });
313
+ });
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // 10. GitHubCicd.rerunWorkflow — triggers rerun
317
+ // ---------------------------------------------------------------------------
318
+
319
+ describe('GitHubCicd.rerunWorkflow', () => {
320
+ it('sends POST to the rerun endpoint and returns success indicator', async () => {
321
+ const calls = [];
322
+ const cicd = new GitHubCicd({
323
+ owner: 'my-org',
324
+ installationToken: 'ghs_test',
325
+ fetchImpl: async (url, options = {}) => {
326
+ calls.push({ url, method: options.method });
327
+ return { ok: true, status: 201, json: async () => null };
328
+ }
329
+ });
330
+
331
+ const result = await cicd.rerunWorkflow({ repo: 'my-repo', runId: 1001 });
332
+
333
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
334
+ assert.equal(calls[0].method, 'POST', 'must use POST method');
335
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/actions/runs/1001/rerun'), `URL must target rerun endpoint, got ${calls[0].url}`);
336
+ assert.ok(result, 'rerunWorkflow must return a result');
337
+ assert.equal(result.triggered, true, 'result.triggered must be true');
338
+ });
339
+ });
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // 11. GitHubCicd.cancelWorkflow — cancels a run
343
+ // ---------------------------------------------------------------------------
344
+
345
+ describe('GitHubCicd.cancelWorkflow', () => {
346
+ it('sends POST to the cancel endpoint and returns success indicator', async () => {
347
+ const calls = [];
348
+ const cicd = new GitHubCicd({
349
+ owner: 'my-org',
350
+ installationToken: 'ghs_test',
351
+ fetchImpl: async (url, options = {}) => {
352
+ calls.push({ url, method: options.method });
353
+ return { ok: true, status: 202, json: async () => null };
354
+ }
355
+ });
356
+
357
+ const result = await cicd.cancelWorkflow({ repo: 'my-repo', runId: 1002 });
358
+
359
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
360
+ assert.equal(calls[0].method, 'POST', 'must use POST method');
361
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/actions/runs/1002/cancel'), `URL must target cancel endpoint, got ${calls[0].url}`);
362
+ assert.ok(result, 'cancelWorkflow must return a result');
363
+ assert.equal(result.cancelled, true, 'result.cancelled must be true');
364
+ });
365
+ });
366
+
367
+ // ---------------------------------------------------------------------------
368
+ // 12. GitHubCicd.createCheck — creates a check run
369
+ // ---------------------------------------------------------------------------
370
+
371
+ describe('GitHubCicd.createCheck', () => {
372
+ it('sends POST to create a check run with name, headSha, and status', async () => {
373
+ const calls = [];
374
+ const cicd = new GitHubCicd({
375
+ owner: 'my-org',
376
+ installationToken: 'ghs_test',
377
+ fetchImpl: async (url, options = {}) => {
378
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
379
+ return {
380
+ ok: true, status: 201,
381
+ json: async () => ({ id: 3001, name: 'my-check', status: 'in_progress', conclusion: null, html_url: 'https://github.com/my-org/repo/runs/3001' })
382
+ };
383
+ }
384
+ });
385
+
386
+ const result = await cicd.createCheck({ repo: 'my-repo', name: 'my-check', headSha: 'abc123', status: 'in_progress' });
387
+
388
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
389
+ assert.equal(calls[0].method, 'POST', 'must use POST method');
390
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/check-runs'), `URL must target check-runs endpoint, got ${calls[0].url}`);
391
+ assert.equal(calls[0].body.name, 'my-check', 'payload must include name');
392
+ assert.equal(calls[0].body.head_sha, 'abc123', 'payload must include head_sha');
393
+ assert.equal(calls[0].body.status, 'in_progress', 'payload must include status');
394
+ assert.ok(result, 'createCheck must return the created check run');
395
+ assert.equal(result.id, 3001, 'result must include check run id');
396
+ });
397
+ });
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // 13. GitHubCicd.updateCheck — updates check conclusion
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe('GitHubCicd.updateCheck', () => {
404
+ it('sends PATCH to update check run conclusion and status', async () => {
405
+ const calls = [];
406
+ const cicd = new GitHubCicd({
407
+ owner: 'my-org',
408
+ installationToken: 'ghs_test',
409
+ fetchImpl: async (url, options = {}) => {
410
+ calls.push({ url, method: options.method, body: options.body ? JSON.parse(options.body) : undefined });
411
+ return {
412
+ ok: true, status: 200,
413
+ json: async () => ({ id: 3001, name: 'my-check', status: 'completed', conclusion: 'success', html_url: '' })
414
+ };
415
+ }
416
+ });
417
+
418
+ const result = await cicd.updateCheck({ repo: 'my-repo', checkRunId: 3001, status: 'completed', conclusion: 'success' });
419
+
420
+ assert.equal(calls.length, 1, 'exactly one HTTP call must be made');
421
+ assert.equal(calls[0].method, 'PATCH', 'must use PATCH method');
422
+ assert.ok(calls[0].url.includes('/repos/my-org/my-repo/check-runs/3001'), `URL must target check-run 3001, got ${calls[0].url}`);
423
+ assert.equal(calls[0].body.status, 'completed', 'payload must include status');
424
+ assert.equal(calls[0].body.conclusion, 'success', 'payload must include conclusion');
425
+ assert.ok(result, 'updateCheck must return the updated check run');
426
+ assert.equal(result.conclusion, 'success', 'result must include conclusion');
427
+ });
428
+ });
429
+
430
+ // ---------------------------------------------------------------------------
431
+ // 14. GitHubCicd — implements cicd interface
432
+ // ---------------------------------------------------------------------------
433
+
434
+ describe('GitHubCicd — interface contract', () => {
435
+ it('exposes all required cicd interface methods', () => {
436
+ const cicd = makeCicd();
437
+ const requiredMethods = [
438
+ 'listWorkflowRuns',
439
+ 'listJobs',
440
+ 'rerunWorkflow',
441
+ 'cancelWorkflow',
442
+ 'createCheck',
443
+ 'updateCheck'
444
+ ];
445
+ for (const method of requiredMethods) {
446
+ assert.equal(typeof cicd[method], 'function', `GitHubCicd must expose method: ${method}`);
447
+ }
448
+ });
449
+
450
+ it('has a role property identifying it as github-cicd', () => {
451
+ const cicd = makeCicd();
452
+ assert.equal(cicd.role, 'github-cicd', 'role must be "github-cicd"');
453
+ });
454
+
455
+ it('throws when required constructor arguments are missing', () => {
456
+ assert.throws(
457
+ () => new GitHubCicd({ installationToken: 'tok', fetchImpl: () => {} }),
458
+ /owner/i,
459
+ 'must throw when owner is missing'
460
+ );
461
+ assert.throws(
462
+ () => new GitHubCicd({ owner: 'org', fetchImpl: () => {} }),
463
+ /installationToken/i,
464
+ 'must throw when installationToken is missing'
465
+ );
466
+ });
467
+ });
468
+
469
+ // ---------------------------------------------------------------------------
470
+ // 15. createGitHubProvider — includes all 3 interfaces
471
+ // ---------------------------------------------------------------------------
472
+
473
+ describe('createGitHubProvider — all 3 interfaces', () => {
474
+ it('returns a provider that can create forge, issueTracker, and cicd instances', () => {
475
+ const provider = createGitHubProvider({
476
+ appId: '12345',
477
+ privateKey: 'test-key',
478
+ installationId: '42'
479
+ });
480
+
481
+ assert.ok(provider, 'createGitHubProvider must return a value');
482
+ assert.equal(provider.type, 'github', 'provider.type must be "github"');
483
+ assert.equal(typeof provider.createForge, 'function', 'provider must expose createForge');
484
+ assert.equal(typeof provider.createIssueTracker, 'function', 'provider must expose createIssueTracker');
485
+ assert.equal(typeof provider.createCicd, 'function', 'provider must expose createCicd');
486
+ });
487
+
488
+ it('createIssueTracker returns a GitHubIssueTracking instance', () => {
489
+ const provider = createGitHubProvider({ appId: '1', privateKey: 'key' });
490
+ const tracker = provider.createIssueTracker({
491
+ owner: 'test-org',
492
+ installationToken: 'ghs_test',
493
+ fetchImpl: async () => ({ ok: true, status: 200, json: async () => [] })
494
+ });
495
+ assert.ok(tracker, 'createIssueTracker must return a value');
496
+ assert.equal(tracker.role, 'github-issue-tracking', 'tracker must have correct role');
497
+ assert.equal(typeof tracker.listIssues, 'function', 'tracker must expose listIssues');
498
+ assert.equal(typeof tracker.createIssue, 'function', 'tracker must expose createIssue');
499
+ });
500
+
501
+ it('createCicd returns a GitHubCicd instance', () => {
502
+ const provider = createGitHubProvider({ appId: '1', privateKey: 'key' });
503
+ const cicd = provider.createCicd({
504
+ owner: 'test-org',
505
+ installationToken: 'ghs_test',
506
+ fetchImpl: async () => ({ ok: true, status: 200, json: async () => ({}) })
507
+ });
508
+ assert.ok(cicd, 'createCicd must return a value');
509
+ assert.equal(cicd.role, 'github-cicd', 'cicd must have correct role');
510
+ assert.equal(typeof cicd.listWorkflowRuns, 'function', 'cicd must expose listWorkflowRuns');
511
+ assert.equal(typeof cicd.createCheck, 'function', 'cicd must expose createCheck');
512
+ });
513
+
514
+ it('exports GITHUB_ISSUE_TRACKING_BOUNDARY and GITHUB_CICD_BOUNDARY', () => {
515
+ assert.ok(GITHUB_ISSUE_TRACKING_BOUNDARY, 'GITHUB_ISSUE_TRACKING_BOUNDARY must be exported');
516
+ assert.equal(GITHUB_ISSUE_TRACKING_BOUNDARY.role, 'github-issue-tracking', 'issue tracking boundary role must match');
517
+ assert.ok(GITHUB_CICD_BOUNDARY, 'GITHUB_CICD_BOUNDARY must be exported');
518
+ assert.equal(GITHUB_CICD_BOUNDARY.role, 'github-cicd', 'cicd boundary role must match');
519
+ });
520
+ });