@a5c-ai/krate 5.0.1-staging.3a341c33c

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 (178) 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 +2455 -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/glossary.md +66 -0
  27. package/docs/agents/implementation-blueprint.md +324 -0
  28. package/docs/agents/implementation-rollout-slices.md +251 -0
  29. package/docs/agents/memory-context-integration-spec.md +194 -0
  30. package/docs/agents/memory-ontology-schema-spec.md +253 -0
  31. package/docs/agents/memory-operations-runbook.md +121 -0
  32. package/docs/agents/mvp-vertical-slice-spec.md +146 -0
  33. package/docs/agents/observability-audit-spec.md +265 -0
  34. package/docs/agents/operator-runbook.md +174 -0
  35. package/docs/agents/org-memory-api-payload-examples.md +333 -0
  36. package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
  37. package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
  38. package/docs/agents/org-memory-ui-implementation-map.md +114 -0
  39. package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
  40. package/docs/agents/org-resource-model-delta-spec.md +111 -0
  41. package/docs/agents/org-route-resource-model-spec.md +183 -0
  42. package/docs/agents/org-scoping-namespace-spec.md +114 -0
  43. package/docs/agents/rbac-secrets-management-spec.md +406 -0
  44. package/docs/agents/repository-page-integration-spec.md +255 -0
  45. package/docs/agents/resource-contract-examples.md +808 -0
  46. package/docs/agents/resource-relationship-map.md +190 -0
  47. package/docs/agents/security-threat-model.md +188 -0
  48. package/docs/agents/shared-memory-company-brain-spec.md +358 -0
  49. package/docs/agents/storage-migration-spec.md +168 -0
  50. package/docs/agents/subagent-orchestration-spec.md +152 -0
  51. package/docs/agents/system-overview.md +88 -0
  52. package/docs/agents/tools-mcp-skills-spec.md +189 -0
  53. package/docs/agents/traceability-matrix.md +79 -0
  54. package/docs/agents/ui-flow-spec.md +211 -0
  55. package/docs/agents/ui-ux-system-spec.md +426 -0
  56. package/docs/agents/workspace-lifecycle-spec.md +166 -0
  57. package/docs/architecture-spec.md +78 -0
  58. package/docs/components/control-plane.md +78 -0
  59. package/docs/components/data-plane.md +69 -0
  60. package/docs/components/hooks-events.md +67 -0
  61. package/docs/components/identity-rbac-policy.md +73 -0
  62. package/docs/components/kubevela-oam.md +70 -0
  63. package/docs/components/operations-publishing.md +81 -0
  64. package/docs/components/runners-ci.md +66 -0
  65. package/docs/components/web-ui.md +94 -0
  66. package/docs/external/README.md +47 -0
  67. package/docs/external/bidirectional-sync-design.md +134 -0
  68. package/docs/external/cicd-interface.md +64 -0
  69. package/docs/external/external-backend-controllers.md +170 -0
  70. package/docs/external/external-backend-crds.md +234 -0
  71. package/docs/external/external-backend-ui-spec.md +151 -0
  72. package/docs/external/external-backend-ux-flows.md +115 -0
  73. package/docs/external/external-object-mapping.md +125 -0
  74. package/docs/external/git-forge-interface.md +68 -0
  75. package/docs/external/github-integration-design.md +151 -0
  76. package/docs/external/issue-tracking-interface.md +66 -0
  77. package/docs/external/provider-capability-manifests.md +204 -0
  78. package/docs/external/provider-catalog.md +139 -0
  79. package/docs/external/provider-rollout-testing.md +78 -0
  80. package/docs/external/research-results.md +48 -0
  81. package/docs/external/security-auth-permissions.md +81 -0
  82. package/docs/external/sync-state-machines.md +108 -0
  83. package/docs/external/unified-external-backend-model.md +107 -0
  84. package/docs/external/user-facing-changes.md +67 -0
  85. package/docs/gaps.md +161 -0
  86. package/docs/install.md +94 -0
  87. package/docs/krate-design.md +334 -0
  88. package/docs/local-minikube.md +55 -0
  89. package/docs/ontology/README.md +32 -0
  90. package/docs/ontology/bounded-contexts.md +29 -0
  91. package/docs/ontology/events-and-hooks.md +32 -0
  92. package/docs/ontology/oam-kubevela.md +32 -0
  93. package/docs/ontology/operations-and-release.md +25 -0
  94. package/docs/ontology/personas-and-actors.md +32 -0
  95. package/docs/ontology/policies-and-invariants.md +33 -0
  96. package/docs/ontology/problem-space.md +30 -0
  97. package/docs/ontology/resource-contracts.md +40 -0
  98. package/docs/ontology/resource-taxonomy.md +42 -0
  99. package/docs/ontology/runners-and-ci.md +29 -0
  100. package/docs/ontology/solution-space.md +24 -0
  101. package/docs/ontology/storage-and-data-boundaries.md +29 -0
  102. package/docs/ontology/validation-matrix.md +24 -0
  103. package/docs/ontology/web-ui-excellent-flows.md +32 -0
  104. package/docs/ontology/workflows.md +39 -0
  105. package/docs/ontology/world.md +35 -0
  106. package/docs/product-requirements.md +62 -0
  107. package/docs/roadmap-mvp.md +87 -0
  108. package/docs/system-requirements.md +90 -0
  109. package/docs/tests/README.md +53 -0
  110. package/docs/tests/agent-qa-plan.md +63 -0
  111. package/docs/tests/browser-ui-tests.md +62 -0
  112. package/docs/tests/ci-quality-gates.md +48 -0
  113. package/docs/tests/coverage-model.md +64 -0
  114. package/docs/tests/e2e-scenario-tests.md +53 -0
  115. package/docs/tests/fixtures-test-data.md +63 -0
  116. package/docs/tests/observability-reliability-tests.md +54 -0
  117. package/docs/tests/product-test-matrix.md +145 -0
  118. package/docs/tests/qa-adoption-roadmap.md +130 -0
  119. package/docs/tests/qa-automation-plan.md +101 -0
  120. package/docs/tests/security-compliance-tests.md +57 -0
  121. package/docs/tests/test-framework-tools.md +88 -0
  122. package/docs/tests/test-suite-layout.md +121 -0
  123. package/docs/tests/unit-integration-tests.md +48 -0
  124. package/docs/todo-kyverno +714 -0
  125. package/docs/user-stories.md +78 -0
  126. package/examples/minikube-demo.yaml +190 -0
  127. package/examples/oam-application.yaml +23 -0
  128. package/examples/policy-kyverno-pr-title.yaml +18 -0
  129. package/package.json +63 -0
  130. package/scripts/build.mjs +29 -0
  131. package/scripts/setup-minikube.mjs +65 -0
  132. package/scripts/smoke.mjs +37 -0
  133. package/scripts/validate-doc-coverage.mjs +152 -0
  134. package/scripts/validate-package.mjs +93 -0
  135. package/scripts/validate-ui.mjs +207 -0
  136. package/src/agent-approval-controller.js +123 -0
  137. package/src/agent-context-bundles.js +242 -0
  138. package/src/agent-dispatch-controller.js +86 -0
  139. package/src/agent-memory-controller.js +374 -0
  140. package/src/agent-mux-client.js +280 -0
  141. package/src/agent-permission-review.js +162 -0
  142. package/src/agent-stack-controller.js +296 -0
  143. package/src/agent-trigger-controller.js +108 -0
  144. package/src/agent-workspace-controller.js +208 -0
  145. package/src/api-controller.js +248 -0
  146. package/src/argocd-gitops.js +43 -0
  147. package/src/auth.js +265 -0
  148. package/src/component-catalog.js +41 -0
  149. package/src/control-plane.js +136 -0
  150. package/src/controller-client.js +38 -0
  151. package/src/controller-ui.js +551 -0
  152. package/src/data-plane.js +178 -0
  153. package/src/gitea-backend.js +95 -0
  154. package/src/handoff.js +98 -0
  155. package/src/hooks-events.js +63 -0
  156. package/src/http-server.js +151 -0
  157. package/src/identity-policy.js +86 -0
  158. package/src/index.js +32 -0
  159. package/src/kubernetes-controller.js +812 -0
  160. package/src/kubernetes-resource-gateway.js +48 -0
  161. package/src/operations.js +112 -0
  162. package/src/resource-model.js +211 -0
  163. package/src/runners-ci.js +48 -0
  164. package/src/runtime.js +196 -0
  165. package/src/web-ui.js +40 -0
  166. package/tests/agent-approval-controller.test.js +173 -0
  167. package/tests/agent-context-bundles.test.js +278 -0
  168. package/tests/agent-dispatch-controller.test.js +176 -0
  169. package/tests/agent-memory-controller.test.js +308 -0
  170. package/tests/agent-mux-client.test.js +204 -0
  171. package/tests/agent-permission-review.test.js +209 -0
  172. package/tests/agent-resources.test.js +228 -0
  173. package/tests/agent-stack-controller.test.js +221 -0
  174. package/tests/agent-trigger-controller.test.js +211 -0
  175. package/tests/agent-workspace-controller.test.js +215 -0
  176. package/tests/deployment.test.js +393 -0
  177. package/tests/e2e/lifecycle.test.js +117 -0
  178. package/tests/krate.test.js +727 -0
@@ -0,0 +1,211 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { createAgentTriggerController, createResource } from '../src/index.js';
4
+
5
+ function makeTriggerRule(name, spec = {}) {
6
+ return createResource('AgentTriggerRule', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: 'default',
8
+ sources: ['ci-failure'],
9
+ agentStack: 'debug-stack',
10
+ taskKind: 'diagnostic',
11
+ ...spec
12
+ });
13
+ }
14
+
15
+ function makeEvent(overrides = {}) {
16
+ return {
17
+ type: 'ci-failure',
18
+ repository: 'myapp',
19
+ ref: 'main',
20
+ actor: 'alice',
21
+ source: { kind: 'Pipeline', name: 'pipeline-123' },
22
+ ...overrides
23
+ };
24
+ }
25
+
26
+ function makeExecution(ruleName, eventUid, phase = 'Dispatched') {
27
+ const exec = createResource('AgentTriggerExecution', { name: `exec-${ruleName}-1`, namespace: 'krate-org-default' }, {
28
+ organizationRef: 'default',
29
+ triggerRule: ruleName,
30
+ sourceEvent: eventUid,
31
+ decision: phase
32
+ });
33
+ exec.status = { phase };
34
+ return exec;
35
+ }
36
+
37
+ // 1. matchRule: event type matches
38
+ test('matchRule: event type matches rule sources', () => {
39
+ const controller = createAgentTriggerController();
40
+ const rule = makeTriggerRule('rule-1', { sources: ['ci-failure'] });
41
+ const event = makeEvent({ type: 'ci-failure' });
42
+
43
+ const result = controller.matchRule(rule, event);
44
+
45
+ assert.equal(result.matches, true);
46
+ assert.equal(result.reason, 'All conditions met');
47
+ });
48
+
49
+ // 2. matchRule: event type doesn't match
50
+ test('matchRule: event type does not match rule sources', () => {
51
+ const controller = createAgentTriggerController();
52
+ const rule = makeTriggerRule('rule-2', { sources: ['ci-failure'] });
53
+ const event = makeEvent({ type: 'webhook' });
54
+
55
+ const result = controller.matchRule(rule, event);
56
+
57
+ assert.equal(result.matches, false);
58
+ assert.ok(result.reason.includes("'webhook'"), 'Reason should mention the event type');
59
+ assert.ok(result.reason.includes('ci-failure'), 'Reason should mention the expected source');
60
+ });
61
+
62
+ // 3. matchRule: repository scope match
63
+ test('matchRule: repository scope matches', () => {
64
+ const controller = createAgentTriggerController();
65
+ const rule = makeTriggerRule('rule-3', { sources: ['ci-failure'], repository: 'myapp' });
66
+ const event = makeEvent({ type: 'ci-failure', repository: 'myapp' });
67
+
68
+ const result = controller.matchRule(rule, event);
69
+
70
+ assert.equal(result.matches, true);
71
+ assert.equal(result.reason, 'All conditions met');
72
+ });
73
+
74
+ // 4. matchRule: repository scope mismatch
75
+ test('matchRule: repository scope mismatch', () => {
76
+ const controller = createAgentTriggerController();
77
+ const rule = makeTriggerRule('rule-4', { sources: ['ci-failure'], repository: 'myapp' });
78
+ const event = makeEvent({ type: 'ci-failure', repository: 'other' });
79
+
80
+ const result = controller.matchRule(rule, event);
81
+
82
+ assert.equal(result.matches, false);
83
+ assert.ok(result.reason.includes("'other'"), 'Reason should mention the event repository');
84
+ assert.ok(result.reason.includes("'myapp'"), 'Reason should mention the rule scope');
85
+ });
86
+
87
+ // 5. matchRule: actor filter
88
+ test('matchRule: actor not in allowedActors', () => {
89
+ const controller = createAgentTriggerController();
90
+ const rule = makeTriggerRule('rule-5', { sources: ['ci-failure'], allowedActors: ['alice'] });
91
+ const event = makeEvent({ type: 'ci-failure', actor: 'bob' });
92
+
93
+ const result = controller.matchRule(rule, event);
94
+
95
+ assert.equal(result.matches, false);
96
+ assert.ok(result.reason.includes("'bob'"), 'Reason should mention the rejected actor');
97
+ });
98
+
99
+ // 6. evaluateEvent: deduplication
100
+ test('evaluateEvent: marks duplicate when existing execution matches', () => {
101
+ const controller = createAgentTriggerController();
102
+ const rule = makeTriggerRule('rule-dedup');
103
+ const event = makeEvent();
104
+ const eventUid = `${event.type}:${event.source.kind}:${event.source.name}`;
105
+ const existingExecution = makeExecution('rule-dedup', eventUid, 'Dispatched');
106
+
107
+ const resources = {
108
+ AgentTriggerRule: [rule],
109
+ AgentTriggerExecution: [existingExecution]
110
+ };
111
+
112
+ const evaluations = controller.evaluateEvent({ event, resources });
113
+
114
+ assert.equal(evaluations.length, 1);
115
+ assert.equal(evaluations[0].matches, true);
116
+ assert.equal(evaluations[0].isDuplicate, true);
117
+ });
118
+
119
+ // 7. processEvent: dispatches matching rules
120
+ test('processEvent: dispatches matching rules via dispatch controller', async () => {
121
+ let dispatchCalled = false;
122
+ const mockDispatchController = {
123
+ async createManualDispatch(input) {
124
+ dispatchCalled = true;
125
+ const run = createResource('AgentDispatchRun', { name: `dispatch-${Date.now()}`, namespace: input.namespace }, {
126
+ organizationRef: input.organizationRef,
127
+ repository: input.repository,
128
+ sourceRefs: input.sourceRefs,
129
+ agentStack: input.agentStack,
130
+ taskKind: input.taskKind
131
+ });
132
+ run.status = { phase: 'Queued' };
133
+ return { error: false, run };
134
+ }
135
+ };
136
+ const controller = createAgentTriggerController({ dispatchController: mockDispatchController });
137
+ const rule = makeTriggerRule('rule-dispatch');
138
+ const event = makeEvent();
139
+ const resources = { AgentTriggerRule: [rule], AgentTriggerExecution: [] };
140
+
141
+ const result = await controller.processEvent({ event, resources, namespace: 'krate-org-default', organizationRef: 'default' });
142
+
143
+ assert.equal(dispatchCalled, true, 'Dispatch controller should be called');
144
+ assert.equal(result.dispatched, 1);
145
+ assert.equal(result.skipped, 0);
146
+ assert.equal(result.processed, 1);
147
+ assert.equal(result.executions.length, 1);
148
+ assert.equal(result.executions[0].status.phase, 'Dispatched');
149
+ assert.ok(result.executions[0].status.dispatchRunRef, 'Execution should reference the dispatch run');
150
+ });
151
+
152
+ // 8. processEvent: skips non-matching
153
+ test('processEvent: skips non-matching rules', async () => {
154
+ const controller = createAgentTriggerController();
155
+ const rule = makeTriggerRule('rule-skip', { sources: ['ci-failure'] });
156
+ const event = makeEvent({ type: 'webhook' });
157
+ const resources = { AgentTriggerRule: [rule], AgentTriggerExecution: [] };
158
+
159
+ const result = await controller.processEvent({ event, resources });
160
+
161
+ assert.equal(result.dispatched, 0);
162
+ assert.equal(result.skipped, 1);
163
+ assert.equal(result.processed, 1);
164
+ assert.equal(result.executions[0].status.phase, 'Skipped');
165
+ });
166
+
167
+ // 9. processEvent: multiple rules match same event
168
+ test('processEvent: multiple rules match same event', async () => {
169
+ let dispatchCount = 0;
170
+ const mockDispatchController = {
171
+ async createManualDispatch(input) {
172
+ dispatchCount++;
173
+ const run = createResource('AgentDispatchRun', { name: `dispatch-${Date.now()}-${dispatchCount}`, namespace: input.namespace }, {
174
+ organizationRef: input.organizationRef,
175
+ repository: input.repository,
176
+ sourceRefs: input.sourceRefs,
177
+ agentStack: input.agentStack,
178
+ taskKind: input.taskKind
179
+ });
180
+ run.status = { phase: 'Queued' };
181
+ return { error: false, run };
182
+ }
183
+ };
184
+ const controller = createAgentTriggerController({ dispatchController: mockDispatchController });
185
+ const rule1 = makeTriggerRule('rule-multi-1', { sources: ['ci-failure'], agentStack: 'stack-a' });
186
+ const rule2 = makeTriggerRule('rule-multi-2', { sources: ['ci-failure'], agentStack: 'stack-b' });
187
+ const event = makeEvent();
188
+ const resources = { AgentTriggerRule: [rule1, rule2], AgentTriggerExecution: [] };
189
+
190
+ const result = await controller.processEvent({ event, resources, namespace: 'krate-org-default', organizationRef: 'default' });
191
+
192
+ assert.equal(dispatchCount, 2, 'Dispatch should be called twice');
193
+ assert.equal(result.dispatched, 2);
194
+ assert.equal(result.skipped, 0);
195
+ assert.equal(result.processed, 2);
196
+ assert.equal(result.executions.length, 2);
197
+ });
198
+
199
+ // 10. processEvent: no rules
200
+ test('processEvent: empty rules array', async () => {
201
+ const controller = createAgentTriggerController();
202
+ const event = makeEvent();
203
+ const resources = { AgentTriggerRule: [], AgentTriggerExecution: [] };
204
+
205
+ const result = await controller.processEvent({ event, resources });
206
+
207
+ assert.equal(result.processed, 0);
208
+ assert.equal(result.dispatched, 0);
209
+ assert.equal(result.skipped, 0);
210
+ assert.equal(result.executions.length, 0);
211
+ });
@@ -0,0 +1,215 @@
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, ownership, phase = 'Active', extra = {}) {
6
+ const workspace = createResource('AgentWorkspace', { name, namespace: 'krate-org-default' }, {
7
+ organizationRef: 'default',
8
+ repository,
9
+ workspacePath: `/workspaces/${repository}/main-${Date.now()}`,
10
+ ownership,
11
+ ...extra
12
+ });
13
+ workspace.status = { phase, createdAt: new Date().toISOString(), boundSessions: [], ...extra.status };
14
+ return workspace;
15
+ }
16
+
17
+ function makeRuntime(name, workspaceRef, status = 'provisioning') {
18
+ const runtime = createResource('AgentWorkspaceRuntime', { 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 AgentWorkspace + AgentWorkspaceRuntime', () => {
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, 'AgentWorkspace');
41
+ assert.equal(result.workspace.spec.repository, 'my-repo');
42
+ assert.equal(result.workspace.spec.ownership, 'run-1');
43
+ assert.ok(result.workspace.spec.workspacePath.includes('my-repo'), 'workspacePath should contain repository');
44
+ assert.ok(result.workspace.spec.workspacePath.includes('feature-1'), 'workspacePath should contain branch');
45
+ assert.equal(result.workspace.status.phase, 'Active');
46
+ assert.ok(Array.isArray(result.workspace.status.boundSessions), 'boundSessions should be an array');
47
+
48
+ assert.ok(result.runtime, 'Should return a runtime resource');
49
+ assert.equal(result.runtime.kind, 'AgentWorkspaceRuntime');
50
+ assert.equal(result.runtime.spec.workspaceRef, result.workspace.metadata.name);
51
+ assert.equal(result.runtime.spec.status, 'provisioning');
52
+ assert.equal(result.runtime.status.phase, 'Provisioning');
53
+ });
54
+
55
+ test('archiveWorkspace sets phase=Archived', () => {
56
+ const controller = createAgentWorkspaceController();
57
+ const existing = makeWorkspace('ws-1', 'my-repo', 'run-1', 'Active');
58
+ const result = controller.archiveWorkspace({
59
+ workspaceName: 'ws-1',
60
+ reason: 'Run completed',
61
+ resources: { AgentWorkspace: [existing] }
62
+ });
63
+
64
+ assert.equal(result.error, false, 'Should succeed');
65
+ assert.equal(result.workspace.status.phase, 'Archived');
66
+ assert.ok(result.workspace.status.archivedAt, 'Should have archivedAt timestamp');
67
+ assert.equal(result.workspace.status.archiveReason, 'Run completed');
68
+ });
69
+
70
+ test('recoverWorkspace sets phase=Active', () => {
71
+ const controller = createAgentWorkspaceController();
72
+ const archived = makeWorkspace('ws-2', 'my-repo', 'run-1', 'Archived', {
73
+ status: { archivedAt: new Date().toISOString(), archiveReason: 'Cleanup' }
74
+ });
75
+ const result = controller.recoverWorkspace({
76
+ workspaceName: 'ws-2',
77
+ resources: { AgentWorkspace: [archived] }
78
+ });
79
+
80
+ assert.equal(result.error, false, 'Should succeed');
81
+ assert.equal(result.workspace.status.phase, 'Active');
82
+ assert.equal(result.workspace.status.archivedAt, undefined, 'archivedAt should be cleared');
83
+ assert.equal(result.workspace.status.archiveReason, undefined, 'archiveReason should be cleared');
84
+ });
85
+
86
+ test('bindSession adds session to boundSessions', () => {
87
+ const controller = createAgentWorkspaceController();
88
+ const existing = makeWorkspace('ws-3', 'my-repo', 'run-2', 'Active');
89
+ const result = controller.bindSession({
90
+ workspaceName: 'ws-3',
91
+ sessionRef: 'session-1',
92
+ agent: 'code-agent',
93
+ namespace: 'krate-org-default',
94
+ organizationRef: 'default',
95
+ resources: { AgentWorkspace: [existing] }
96
+ });
97
+
98
+ assert.equal(result.error, false, 'Should succeed');
99
+ assert.ok(result.workspace, 'Should return updated workspace');
100
+ assert.equal(result.workspace.status.boundSessions.length, 1, 'Should have one bound session');
101
+ assert.equal(result.workspace.status.boundSessions[0].sessionRef, 'session-1');
102
+ assert.equal(result.workspace.status.boundSessions[0].agent, 'code-agent');
103
+ assert.ok(result.workspace.status.boundSessions[0].boundAt, 'Should have boundAt timestamp');
104
+ });
105
+
106
+ test('linkWorkItem creates WorkItemWorkspaceLink', () => {
107
+ const controller = createAgentWorkspaceController();
108
+ const result = controller.linkWorkItem({
109
+ workspaceName: 'ws-4',
110
+ workItemRef: 'issue-42',
111
+ workItemKind: 'Issue',
112
+ namespace: 'krate-org-default',
113
+ organizationRef: 'default'
114
+ });
115
+
116
+ assert.equal(result.error, false, 'Should succeed');
117
+ assert.ok(result.link, 'Should return a link resource');
118
+ assert.equal(result.link.kind, 'WorkItemWorkspaceLink');
119
+ assert.equal(result.link.spec.workItemRef, 'issue-42');
120
+ assert.equal(result.link.spec.workItemKind, 'Issue');
121
+ assert.equal(result.link.spec.workspace, 'ws-4');
122
+ assert.equal(result.link.spec.organizationRef, 'default');
123
+ });
124
+
125
+ test('linkWorkItemToSession creates WorkItemSessionLink', () => {
126
+ const controller = createAgentWorkspaceController();
127
+ const result = controller.linkWorkItemToSession({
128
+ workItemRef: 'pr-10',
129
+ workItemKind: 'PullRequest',
130
+ sessionRef: 'session-5',
131
+ namespace: 'krate-org-default',
132
+ organizationRef: 'default'
133
+ });
134
+
135
+ assert.equal(result.error, false, 'Should succeed');
136
+ assert.ok(result.link, 'Should return a link resource');
137
+ assert.equal(result.link.kind, 'WorkItemSessionLink');
138
+ assert.equal(result.link.spec.workItemRef, 'pr-10');
139
+ assert.equal(result.link.spec.workItemKind, 'PullRequest');
140
+ assert.equal(result.link.spec.agentSession, 'session-5');
141
+ assert.equal(result.link.spec.organizationRef, 'default');
142
+ });
143
+
144
+ test('getWorkspaceStatus returns full bindings', () => {
145
+ const controller = createAgentWorkspaceController();
146
+ const ws = makeWorkspace('ws-5', 'my-repo', 'run-3', 'Active', {
147
+ status: { boundSessions: [{ sessionRef: 'session-10', boundAt: new Date().toISOString() }] }
148
+ });
149
+ const rt = makeRuntime('rt-ws-5', 'ws-5', 'provisioning');
150
+ const link = createResource('WorkItemWorkspaceLink', { name: 'link-1', namespace: 'krate-org-default' }, {
151
+ organizationRef: 'default',
152
+ workItemRef: 'issue-99',
153
+ workspace: 'ws-5'
154
+ });
155
+
156
+ const result = controller.getWorkspaceStatus({
157
+ workspaceName: 'ws-5',
158
+ resources: {
159
+ AgentWorkspace: [ws],
160
+ AgentWorkspaceRuntime: [rt],
161
+ AgentSession: [],
162
+ WorkItemWorkspaceLink: [link]
163
+ }
164
+ });
165
+
166
+ assert.equal(result.error, false, 'Should succeed');
167
+ assert.ok(result.workspace, 'Should return workspace');
168
+ assert.equal(result.workspace.metadata.name, 'ws-5');
169
+ assert.ok(result.runtime, 'Should return runtime');
170
+ assert.equal(result.runtime.spec.workspaceRef, 'ws-5');
171
+ assert.equal(result.sessions.length, 1, 'Should return bound sessions');
172
+ assert.equal(result.workItems.length, 1, 'Should return linked work items');
173
+ assert.equal(result.workItems[0].spec.workItemRef, 'issue-99');
174
+ });
175
+
176
+ test('listWorkspacesForRepo filters by repository', () => {
177
+ const controller = createAgentWorkspaceController();
178
+ const ws1 = makeWorkspace('ws-a', 'repo-alpha', 'run-a');
179
+ const ws2 = makeWorkspace('ws-b', 'repo-beta', 'run-b');
180
+ const ws3 = makeWorkspace('ws-c', 'repo-alpha', 'run-c');
181
+
182
+ const result = controller.listWorkspacesForRepo({
183
+ repository: 'repo-alpha',
184
+ resources: { AgentWorkspace: [ws1, ws2, ws3] }
185
+ });
186
+
187
+ assert.equal(result.length, 2, 'Should return workspaces for repo-alpha only');
188
+ assert.ok(result.every((w) => w.spec.repository === 'repo-alpha'), 'All should belong to repo-alpha');
189
+ });
190
+
191
+ test('archiveWorkspace on nonexistent returns error', () => {
192
+ const controller = createAgentWorkspaceController();
193
+ const result = controller.archiveWorkspace({
194
+ workspaceName: 'ws-nonexistent',
195
+ reason: 'Cleanup',
196
+ resources: { AgentWorkspace: [] }
197
+ });
198
+
199
+ assert.equal(result.error, true, 'Should fail');
200
+ assert.equal(result.reason, 'not-found');
201
+ assert.ok(result.message.includes('ws-nonexistent'), 'Message should mention the workspace name');
202
+ });
203
+
204
+ test('recoverWorkspace on non-archived returns error', () => {
205
+ const controller = createAgentWorkspaceController();
206
+ const active = makeWorkspace('ws-active', 'my-repo', 'run-x', 'Active');
207
+ const result = controller.recoverWorkspace({
208
+ workspaceName: 'ws-active',
209
+ resources: { AgentWorkspace: [active] }
210
+ });
211
+
212
+ assert.equal(result.error, true, 'Should fail');
213
+ assert.equal(result.reason, 'not-archived');
214
+ assert.ok(result.message.includes('not archived'), 'Message should indicate not archived');
215
+ });