@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,318 @@
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, phase = 'Ready', extra = {}) {
6
+ const workspace = createResource('KrateWorkspace', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: extra.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(), volumeStatus: 'Bound', ...extra.status };
14
+ if (extra.associations) {
15
+ workspace.spec.associations = extra.associations;
16
+ }
17
+ return workspace;
18
+ }
19
+
20
+ function makeRun(name, workspaceRef, phase = 'Succeeded', extra = {}) {
21
+ const run = createResource('AgentDispatchRun', { name, namespace: 'krate-org-default' }, {
22
+ organizationRef: 'default',
23
+ repository: extra.repository || 'my-repo',
24
+ sourceRefs: extra.sourceRefs || ['main'],
25
+ agentStack: extra.agentStack || 'code-agent',
26
+ taskKind: extra.taskKind || 'diagnostic',
27
+ workspaceRef,
28
+ });
29
+ run.status = {
30
+ phase,
31
+ workspaceRef,
32
+ createdAt: extra.createdAt || new Date().toISOString(),
33
+ ...extra.status,
34
+ };
35
+ return run;
36
+ }
37
+
38
+ // --- launchCodespace ---
39
+
40
+ test('launchCodespace returns valid pod spec with PVC mount', () => {
41
+ const controller = createAgentWorkspaceController();
42
+ const ws = makeWorkspace('ws-dev', 'my-repo');
43
+ const result = controller.launchCodespace(ws);
44
+
45
+ assert.equal(result.error, false);
46
+ assert.ok(result.podSpec, 'Should return podSpec');
47
+ assert.equal(result.podSpec.kind, 'Pod');
48
+ assert.equal(result.podSpec.metadata.name, 'codespace-ws-dev');
49
+
50
+ // Verify PVC mount
51
+ const volumes = result.podSpec.spec.volumes;
52
+ assert.ok(volumes.some((v) => v.persistentVolumeClaim?.claimName === 'krate-ws-ws-dev'), 'PVC should be mounted');
53
+ const container = result.podSpec.spec.containers[0];
54
+ assert.ok(container.volumeMounts.some((m) => m.mountPath === '/workspace'), 'Should mount at /workspace');
55
+ });
56
+
57
+ test('launchCodespace uses custom image when provided', () => {
58
+ const controller = createAgentWorkspaceController();
59
+ const ws = makeWorkspace('ws-custom', 'my-repo');
60
+ const result = controller.launchCodespace(ws, { image: 'gitpod/openvscode-server:latest' });
61
+
62
+ assert.equal(result.error, false);
63
+ assert.equal(result.podSpec.spec.containers[0].image, 'gitpod/openvscode-server:latest');
64
+ });
65
+
66
+ test('launchCodespace rejects if codespace already running', () => {
67
+ const controller = createAgentWorkspaceController();
68
+ const ws = makeWorkspace('ws-running', 'my-repo', 'Ready', {
69
+ status: { codespace: { running: true } },
70
+ });
71
+ const result = controller.launchCodespace(ws);
72
+
73
+ assert.equal(result.error, true);
74
+ assert.equal(result.reason, 'codespace-already-running');
75
+ });
76
+
77
+ test('launchCodespace pod spec has correct resource limits', () => {
78
+ const controller = createAgentWorkspaceController();
79
+ const ws = makeWorkspace('ws-limits', 'my-repo');
80
+ const result = controller.launchCodespace(ws, { cpu: '2', memory: '4Gi' });
81
+
82
+ assert.equal(result.error, false);
83
+ const container = result.podSpec.spec.containers[0];
84
+ assert.equal(container.resources.limits.cpu, '2');
85
+ assert.equal(container.resources.limits.memory, '4Gi');
86
+ });
87
+
88
+ test('launchCodespace pod spec has correct env vars', () => {
89
+ const controller = createAgentWorkspaceController();
90
+ const ws = makeWorkspace('ws-env', 'my-repo');
91
+ const result = controller.launchCodespace(ws, { gitAuthorName: 'Test User' });
92
+
93
+ assert.equal(result.error, false);
94
+ const container = result.podSpec.spec.containers[0];
95
+ const envMap = {};
96
+ for (const e of container.env) envMap[e.name] = e.value || e.valueFrom;
97
+
98
+ assert.equal(envMap.KRATE_WORKSPACE, 'ws-env');
99
+ assert.equal(envMap.KRATE_ORG, 'default');
100
+ assert.equal(envMap.GIT_AUTHOR_NAME, 'Test User');
101
+ assert.ok(envMap.GIT_AUTHOR_EMAIL, 'Should have GIT_AUTHOR_EMAIL');
102
+ });
103
+
104
+ test('launchCodespace service spec exposes port 8080', () => {
105
+ const controller = createAgentWorkspaceController();
106
+ const ws = makeWorkspace('ws-svc', 'my-repo');
107
+ const result = controller.launchCodespace(ws);
108
+
109
+ assert.equal(result.error, false);
110
+ assert.ok(result.serviceSpec, 'Should return serviceSpec');
111
+ assert.equal(result.serviceSpec.kind, 'Service');
112
+ const ports = result.serviceSpec.spec.ports;
113
+ assert.ok(ports.some((p) => p.port === 8080), 'Service should expose port 8080');
114
+ });
115
+
116
+ test('launchCodespace returns codespace URL', () => {
117
+ const controller = createAgentWorkspaceController();
118
+ const ws = makeWorkspace('ws-url', 'my-repo');
119
+ const result = controller.launchCodespace(ws);
120
+
121
+ assert.equal(result.error, false);
122
+ assert.ok(result.codespaceUrl, 'Should return codespaceUrl');
123
+ assert.ok(result.codespaceUrl.includes('codespace-svc-ws-url'), 'URL should reference service name');
124
+ assert.ok(result.codespaceUrl.includes('8080'), 'URL should include port');
125
+ });
126
+
127
+ test('launchCodespace includes password secret ref when provided', () => {
128
+ const controller = createAgentWorkspaceController();
129
+ const ws = makeWorkspace('ws-pw', 'my-repo');
130
+ const secretRef = { name: 'my-secret', key: 'password' };
131
+ const result = controller.launchCodespace(ws, { passwordSecretRef: secretRef });
132
+
133
+ assert.equal(result.error, false);
134
+ const container = result.podSpec.spec.containers[0];
135
+ const pwEnv = container.env.find((e) => e.name === 'PASSWORD');
136
+ assert.ok(pwEnv, 'Should have PASSWORD env');
137
+ assert.deepEqual(pwEnv.valueFrom.secretKeyRef, secretRef);
138
+ });
139
+
140
+ test('launchCodespace returns error when workspace is null', () => {
141
+ const controller = createAgentWorkspaceController();
142
+ const result = controller.launchCodespace(null);
143
+
144
+ assert.equal(result.error, true);
145
+ assert.equal(result.reason, 'missing-workspace');
146
+ });
147
+
148
+ // --- stopCodespace ---
149
+
150
+ test('stopCodespace returns delete manifests for pod and service', () => {
151
+ const controller = createAgentWorkspaceController();
152
+ const ws = makeWorkspace('ws-stop', 'my-repo');
153
+ const result = controller.stopCodespace(ws);
154
+
155
+ assert.equal(result.error, false);
156
+ assert.ok(result.podDeleteManifest, 'Should return pod delete manifest');
157
+ assert.equal(result.podDeleteManifest.kind, 'Pod');
158
+ assert.equal(result.podDeleteManifest.metadata.name, 'codespace-ws-stop');
159
+ assert.equal(result.podDeleteManifest.action, 'delete');
160
+
161
+ assert.ok(result.serviceDeleteManifest, 'Should return service delete manifest');
162
+ assert.equal(result.serviceDeleteManifest.kind, 'Service');
163
+ assert.equal(result.serviceDeleteManifest.metadata.name, 'codespace-svc-ws-stop');
164
+ assert.equal(result.serviceDeleteManifest.action, 'delete');
165
+ });
166
+
167
+ // --- getCodespaceStatus ---
168
+
169
+ test('getCodespaceStatus returns running state when pod is Running', () => {
170
+ const controller = createAgentWorkspaceController();
171
+ const ws = makeWorkspace('ws-status', 'my-repo');
172
+ const result = controller.getCodespaceStatus(ws, { phase: 'Running', startTime: '2026-05-13T10:00:00Z', connectedUsers: 2 });
173
+
174
+ assert.equal(result.error, false);
175
+ assert.equal(result.running, true);
176
+ assert.ok(result.url, 'Should have URL when running');
177
+ assert.equal(result.port, 8080);
178
+ assert.equal(result.connectedUsers, 2);
179
+ assert.equal(result.startTime, '2026-05-13T10:00:00Z');
180
+ });
181
+
182
+ test('getCodespaceStatus returns not running when no pod status', () => {
183
+ const controller = createAgentWorkspaceController();
184
+ const ws = makeWorkspace('ws-status-off', 'my-repo');
185
+ const result = controller.getCodespaceStatus(ws);
186
+
187
+ assert.equal(result.error, false);
188
+ assert.equal(result.running, false);
189
+ assert.equal(result.url, null);
190
+ assert.equal(result.phase, 'Unknown');
191
+ });
192
+
193
+ // --- addAssociation ---
194
+
195
+ test('addAssociation adds to associations array', () => {
196
+ const controller = createAgentWorkspaceController();
197
+ const ws = makeWorkspace('ws-assoc', 'my-repo');
198
+ const result = controller.addAssociation(ws, { kind: 'User', name: 'alice' });
199
+
200
+ assert.equal(result.error, false);
201
+ assert.equal(result.workspace.spec.associations.length, 1);
202
+ assert.equal(result.workspace.spec.associations[0].kind, 'User');
203
+ assert.equal(result.workspace.spec.associations[0].name, 'alice');
204
+ assert.ok(result.workspace.spec.associations[0].addedAt, 'Should have addedAt timestamp');
205
+ });
206
+
207
+ test('addAssociation rejects duplicate', () => {
208
+ const controller = createAgentWorkspaceController();
209
+ const ws = makeWorkspace('ws-dup', 'my-repo', 'Ready', {
210
+ associations: [{ kind: 'User', name: 'alice', addedAt: new Date().toISOString() }],
211
+ });
212
+ const result = controller.addAssociation(ws, { kind: 'User', name: 'alice' });
213
+
214
+ assert.equal(result.error, true);
215
+ assert.equal(result.reason, 'duplicate-association');
216
+ });
217
+
218
+ test('addAssociation rejects invalid kind', () => {
219
+ const controller = createAgentWorkspaceController();
220
+ const ws = makeWorkspace('ws-bad-kind', 'my-repo');
221
+ const result = controller.addAssociation(ws, { kind: 'InvalidKind', name: 'foo' });
222
+
223
+ assert.equal(result.error, true);
224
+ assert.equal(result.reason, 'invalid-ref-kind');
225
+ });
226
+
227
+ // --- removeAssociation ---
228
+
229
+ test('removeAssociation removes from array', () => {
230
+ const controller = createAgentWorkspaceController();
231
+ const ws = makeWorkspace('ws-rm', 'my-repo', 'Ready', {
232
+ associations: [
233
+ { kind: 'User', name: 'alice', addedAt: new Date().toISOString() },
234
+ { kind: 'AgentSession', name: 'sess-1', addedAt: new Date().toISOString() },
235
+ ],
236
+ });
237
+ const result = controller.removeAssociation(ws, { kind: 'User', name: 'alice' });
238
+
239
+ assert.equal(result.error, false);
240
+ assert.equal(result.workspace.spec.associations.length, 1);
241
+ assert.equal(result.workspace.spec.associations[0].kind, 'AgentSession');
242
+ });
243
+
244
+ test('removeAssociation returns error if not found', () => {
245
+ const controller = createAgentWorkspaceController();
246
+ const ws = makeWorkspace('ws-rm-nf', 'my-repo');
247
+ const result = controller.removeAssociation(ws, { kind: 'User', name: 'nobody' });
248
+
249
+ assert.equal(result.error, true);
250
+ assert.equal(result.reason, 'not-found');
251
+ });
252
+
253
+ // --- listAssociations ---
254
+
255
+ test('listAssociations returns all with kind/name', () => {
256
+ const controller = createAgentWorkspaceController();
257
+ const ws = makeWorkspace('ws-list', 'my-repo', 'Ready', {
258
+ associations: [
259
+ { kind: 'User', name: 'alice', addedAt: '2026-01-01T00:00:00Z' },
260
+ { kind: 'AgentDispatchRun', name: 'run-1', addedAt: '2026-01-02T00:00:00Z' },
261
+ { kind: 'AgentSession', name: 'sess-1', addedAt: '2026-01-03T00:00:00Z' },
262
+ ],
263
+ });
264
+ const result = controller.listAssociations(ws);
265
+
266
+ assert.equal(result.error, false);
267
+ assert.equal(result.associations.length, 3);
268
+ assert.equal(result.associations[0].kind, 'User');
269
+ assert.equal(result.associations[1].kind, 'AgentDispatchRun');
270
+ assert.equal(result.associations[2].kind, 'AgentSession');
271
+ });
272
+
273
+ // --- getWorkspaceRuns ---
274
+
275
+ test('getWorkspaceRuns filters active vs history', () => {
276
+ const controller = createAgentWorkspaceController();
277
+ const ws = makeWorkspace('ws-runs', 'my-repo');
278
+
279
+ const activeRun = makeRun('run-active', 'ws-runs', 'Running');
280
+ const doneRun = makeRun('run-done', 'ws-runs', 'Succeeded');
281
+ const failedRun = makeRun('run-failed', 'ws-runs', 'Failed');
282
+ const unrelatedRun = makeRun('run-other', 'ws-other', 'Running');
283
+
284
+ const result = controller.getWorkspaceRuns(ws, [activeRun, doneRun, failedRun, unrelatedRun]);
285
+
286
+ assert.equal(result.error, false);
287
+ assert.equal(result.active.length, 1, 'Should have one active run');
288
+ assert.equal(result.active[0].metadata.name, 'run-active');
289
+ assert.equal(result.history.length, 2, 'Should have two history runs');
290
+ });
291
+
292
+ test('getWorkspaceRuns finds runs by association', () => {
293
+ const controller = createAgentWorkspaceController();
294
+ const ws = makeWorkspace('ws-assoc-runs', 'my-repo', 'Ready', {
295
+ associations: [{ kind: 'AgentDispatchRun', name: 'run-linked', addedAt: new Date().toISOString() }],
296
+ });
297
+
298
+ // This run has no workspaceRef but is linked via association
299
+ const linkedRun = makeRun('run-linked', null, 'Succeeded');
300
+ delete linkedRun.status.workspaceRef;
301
+ delete linkedRun.spec.workspaceRef;
302
+
303
+ const result = controller.getWorkspaceRuns(ws, [linkedRun]);
304
+
305
+ assert.equal(result.error, false);
306
+ assert.equal(result.history.length, 1, 'Should find run via association');
307
+ assert.equal(result.history[0].metadata.name, 'run-linked');
308
+ });
309
+
310
+ test('getWorkspaceRuns returns empty when no matching runs', () => {
311
+ const controller = createAgentWorkspaceController();
312
+ const ws = makeWorkspace('ws-empty', 'my-repo');
313
+ const result = controller.getWorkspaceRuns(ws, []);
314
+
315
+ assert.equal(result.error, false);
316
+ assert.equal(result.active.length, 0);
317
+ assert.equal(result.history.length, 0);
318
+ });